Decoupled Packet Handlers (#315)

## Description

Closes #296 

- Redesigned the packet decoder to return packet _frames_ which are just
the packet ID + data in raw form.
- Made packet frame decoding happen in the client's tokio task. This has
a few advantages:
- Packet frame decoding (decompression + decryption + more) can happen
in parallel.
- Because packets are parsed as soon as they arrive, an accurate
timestamp can be included with the packet. This enables us to implement
client ping calculation accurately.
- `PacketEvent`s are now sent in the event loop instead of a giant match
on the serverbound packets. This is good because:
- Packets can now be handled from completely decoupled systems by
reading `PacketEvent` events.
- The entire packet is available in binary form to users, so we don't
need to worry about losing information when transforming packets to
events. I.e. an escape hatch is always available.
  - The separate packet handlers can run in parallel thanks to bevy_ecs.
- The inventory packet handler systems have been unified and moved
completely to the inventory module. This also fixed some issues where
certain inventory events could _only_ be handled one tick late.
- Reorganized the client module and moved things into submodules.
- The "default event handler" has been removed in favor of making
clients a superset of `PlayerEntityBundle`. It is no longer necessary to
insert `PlayerEntityBundle` when clients join. This does mean you can't
insert other entity types on the client, but that design doesn't work
for a variety of reasons. We will need an "entity visibility" system
later anyway.

## Test Plan

Steps:
1. Run examples and tests.
This commit is contained in:
Ryan Johnson 2023-04-08 12:55:31 -07:00 committed by GitHub
parent 1c405ab63c
commit 11ba70586e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
59 changed files with 2875 additions and 3521 deletions

View file

@ -7,14 +7,13 @@ use owo_colors::{OwoColorize, Style};
use regex::Regex;
use serde::{Deserialize, Serialize};
use time::OffsetDateTime;
use valence_protocol::codec::PacketDecoder;
use valence_protocol::decoder::PacketDecoder;
use valence_protocol::packet::c2s::handshake::HandshakeC2s;
use valence_protocol::packet::c2s::login::{LoginHelloC2s, LoginKeyC2s};
use valence_protocol::packet::c2s::status::{QueryPingC2s, QueryRequestC2s};
use valence_protocol::packet::s2c::login::LoginSuccessS2c;
use valence_protocol::packet::s2c::status::{QueryPongS2c, QueryResponseS2c};
use valence_protocol::packet::{C2sPlayPacket, S2cLoginPacket, S2cPlayPacket};
use valence_protocol::raw::RawPacket;
use crate::packet_widget::{systemtime_strftime, PacketDirection};
use crate::MetaPacket;
@ -78,7 +77,7 @@ pub struct Packet {
pub(crate) id: usize,
pub(crate) direction: PacketDirection,
pub(crate) selected: bool,
pub(crate) use_compression: bool,
pub(crate) compression_threshold: Option<u32>,
pub(crate) packet_data: Vec<u8>,
pub(crate) stage: Stage,
pub(crate) packet_type: i32,
@ -93,169 +92,58 @@ impl Packet {
pub fn get_raw_packet(&self) -> Vec<u8> {
let mut dec = PacketDecoder::new();
dec.set_compression(self.use_compression);
dec.set_compression(self.compression_threshold);
dec.queue_slice(&self.packet_data);
let pkt = match dec.try_next_packet::<RawPacket>() {
Ok(Some(pkt)) => pkt,
Ok(None) => return vec![],
match dec.try_next_packet() {
Ok(Some(data)) => data.into(),
Ok(None) => vec![],
Err(e) => {
eprintln!("Error decoding packet: {e}");
return vec![];
eprintln!("Error decoding packet: {e:#}");
vec![]
}
};
pkt.0.to_vec()
}
}
pub fn get_packet_string(&self, formatted: bool) -> String {
let mut dec = PacketDecoder::new();
dec.set_compression(self.use_compression);
dec.set_compression(self.compression_threshold);
dec.queue_slice(&self.packet_data);
macro_rules! get {
($packet:ident) => {
match dec.try_next_packet() {
Ok(Some(frame)) => {
if let Ok(pkt) =
<$packet as valence_protocol::Packet>::decode_packet(&mut &frame[..])
{
if formatted {
format!("{pkt:#?}")
} else {
format!("{pkt:?}")
}
} else {
stringify!($packet).into()
}
}
Ok(None) => stringify!($packet).into(),
Err(e) => format!("{e:#}"),
}
};
}
match self.stage {
Stage::HandshakeC2s => {
let pkt = match dec.try_next_packet::<HandshakeC2s>() {
Ok(Some(pkt)) => pkt,
Ok(None) => return "HandshakeC2s".to_string(),
Err(err) => return format!("{:?}", err),
};
if formatted {
format!("{pkt:#?}")
} else {
format!("{pkt:?}")
}
}
Stage::QueryRequestC2s => {
let pkt = match dec.try_next_packet::<QueryRequestC2s>() {
Ok(Some(pkt)) => pkt,
Ok(None) => return "QueryRequestC2s".to_string(),
Err(err) => return format!("{:?}", err),
};
if formatted {
format!("{pkt:#?}")
} else {
format!("{pkt:?}")
}
}
Stage::QueryResponseS2c => {
let pkt = match dec.try_next_packet::<QueryResponseS2c>() {
Ok(Some(pkt)) => pkt,
Ok(None) => return "QueryResponseS2c".to_string(),
Err(err) => return format!("{:?}", err),
};
if formatted {
format!("{pkt:#?}")
} else {
format!("{pkt:?}")
}
}
Stage::QueryPingC2s => {
let pkt = match dec.try_next_packet::<QueryPingC2s>() {
Ok(Some(pkt)) => pkt,
Ok(None) => return "QueryPingC2s".to_string(),
Err(err) => return format!("{:?}", err),
};
if formatted {
format!("{pkt:#?}")
} else {
format!("{pkt:?}")
}
}
Stage::QueryPongS2c => {
let pkt = match dec.try_next_packet::<QueryPongS2c>() {
Ok(Some(pkt)) => pkt,
Ok(None) => return "QueryPongS2c".to_string(),
Err(err) => return format!("{:?}", err),
};
if formatted {
format!("{pkt:#?}")
} else {
format!("{pkt:?}")
}
}
Stage::LoginHelloC2s => {
let pkt = match dec.try_next_packet::<LoginHelloC2s>() {
Ok(Some(pkt)) => pkt,
Ok(None) => return "LoginHelloC2s".to_string(),
Err(err) => return format!("{:?}", err),
};
if formatted {
format!("{pkt:#?}")
} else {
format!("{pkt:?}")
}
}
Stage::S2cLoginPacket => {
let pkt = match dec.try_next_packet::<S2cLoginPacket>() {
Ok(Some(pkt)) => pkt,
Ok(None) => return "S2cLoginPacket".to_string(),
Err(err) => return format!("{:?}", err),
};
if formatted {
format!("{pkt:#?}")
} else {
format!("{pkt:?}")
}
}
Stage::LoginKeyC2s => {
let pkt = match dec.try_next_packet::<LoginKeyC2s>() {
Ok(Some(pkt)) => pkt,
Ok(None) => return "LoginKeyC2s".to_string(),
Err(err) => return format!("{:?}", err),
};
if formatted {
format!("{pkt:#?}")
} else {
format!("{pkt:?}")
}
}
Stage::LoginSuccessS2c => {
let pkt = match dec.try_next_packet::<LoginSuccessS2c>() {
Ok(Some(pkt)) => pkt,
Ok(None) => return "LoginSuccessS2c".to_string(),
Err(err) => return format!("{:?}", err),
};
if formatted {
format!("{pkt:#?}")
} else {
format!("{pkt:?}")
}
}
Stage::C2sPlayPacket => {
let pkt = match dec.try_next_packet::<C2sPlayPacket>() {
Ok(Some(pkt)) => pkt,
Ok(None) => return "C2sPlayPacket".to_string(),
Err(err) => return format!("{:?}", err),
};
if formatted {
format!("{pkt:#?}")
} else {
format!("{pkt:?}")
}
}
Stage::S2cPlayPacket => {
let pkt = match dec.try_next_packet::<S2cPlayPacket>() {
Ok(Some(pkt)) => pkt,
Ok(None) => return "S2cPlayPacket".to_string(),
Err(err) => return format!("{:?}", err),
};
if formatted {
format!("{pkt:#?}")
} else {
format!("{pkt:?}")
}
}
Stage::HandshakeC2s => get!(HandshakeC2s),
Stage::QueryRequestC2s => get!(QueryRequestC2s),
Stage::QueryResponseS2c => get!(QueryResponseS2c),
Stage::QueryPingC2s => get!(QueryPingC2s),
Stage::QueryPongS2c => get!(QueryPongS2c),
Stage::LoginHelloC2s => get!(LoginHelloC2s),
Stage::S2cLoginPacket => get!(S2cLoginPacket),
Stage::LoginKeyC2s => get!(LoginKeyC2s),
Stage::LoginSuccessS2c => get!(LoginSuccessS2c),
Stage::C2sPlayPacket => get!(C2sPlayPacket),
Stage::S2cPlayPacket => get!(S2cPlayPacket),
}
}
}

View file

@ -26,7 +26,9 @@ use tokio::net::{TcpListener, TcpStream};
use tokio::sync::Semaphore;
use tokio::task::JoinHandle;
use tracing_subscriber::filter::LevelFilter;
use valence_protocol::codec::{PacketDecoder, PacketEncoder};
use valence_protocol::bytes::BytesMut;
use valence_protocol::decoder::PacketDecoder;
use valence_protocol::encoder::PacketEncoder;
use valence_protocol::packet::c2s::handshake::handshake::NextState;
use valence_protocol::packet::c2s::handshake::HandshakeC2s;
use valence_protocol::packet::c2s::login::{LoginHelloC2s, LoginKeyC2s};
@ -189,6 +191,7 @@ async fn handle_connection(
write: client_write,
direction: PacketDirection::ServerToClient,
context: context.clone(),
frame: BytesMut::new(),
};
let mut c2s = State {
@ -198,6 +201,7 @@ async fn handle_connection(
write: server_write,
direction: PacketDirection::ClientToServer,
context: context.clone(),
frame: BytesMut::new(),
};
let handshake: HandshakeC2s = c2s.rw_packet(Stage::HandshakeC2s).await?;
@ -241,9 +245,9 @@ async fn handle_connection(
let threshold = pkt.threshold.0 as u32;
s2c.enc.set_compression(Some(threshold));
s2c.dec.set_compression(true);
s2c.dec.set_compression(Some(threshold));
c2s.enc.set_compression(Some(threshold));
c2s.dec.set_compression(true);
c2s.dec.set_compression(Some(threshold));
s2c.rw_packet::<LoginSuccessS2c>(Stage::LoginSuccessS2c)
.await?;

View file

@ -4,7 +4,9 @@ use std::sync::Arc;
use time::OffsetDateTime;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::tcp::{OwnedReadHalf, OwnedWriteHalf};
use valence_protocol::codec::{PacketDecoder, PacketEncoder};
use valence_protocol::bytes::BytesMut;
use valence_protocol::decoder::{decode_packet, PacketDecoder};
use valence_protocol::encoder::PacketEncoder;
use valence_protocol::Packet as ValencePacket;
use crate::context::{Context, Packet, Stage};
@ -15,6 +17,7 @@ pub struct State {
pub context: Arc<Context>,
pub enc: PacketEncoder,
pub dec: PacketDecoder,
pub frame: BytesMut,
pub read: OwnedReadHalf,
pub write: OwnedWriteHalf,
}
@ -24,7 +27,37 @@ impl State {
where
P: ValencePacket<'a>,
{
while !self.dec.has_next_packet()? {
loop {
if let Some(frame) = self.dec.try_next_packet()? {
self.frame = frame;
let pkt: P = decode_packet(&self.frame)?;
self.enc.append_packet(&pkt)?;
let bytes = self.enc.take();
self.write.write_all(&bytes).await?;
let time = match OffsetDateTime::now_local() {
Ok(time) => time,
Err(_) => OffsetDateTime::now_utc(),
};
self.context.add(Packet {
id: 0, // updated when added to context
direction: self.direction.clone(),
compression_threshold: self.dec.compression(),
packet_data: bytes.to_vec(),
stage,
created_at: time,
selected: false,
packet_type: pkt.packet_id(),
packet_name: pkt.packet_name().to_string(),
});
return Ok(pkt);
}
self.dec.reserve(4096);
let mut buf = self.dec.take_capacity();
@ -34,32 +67,5 @@ impl State {
self.dec.queue_bytes(buf);
}
let has_compression = self.dec.compression();
let pkt: P = self.dec.try_next_packet()?.unwrap();
self.enc.append_packet(&pkt)?;
let bytes = self.enc.take();
self.write.write_all(&bytes).await?;
let time = match OffsetDateTime::now_local() {
Ok(time) => time,
Err(_) => OffsetDateTime::now_utc(),
};
self.context.add(Packet {
id: 0, // updated when added to context
direction: self.direction.clone(),
use_compression: has_compression,
packet_data: bytes.to_vec(),
stage,
created_at: time,
selected: false,
packet_type: pkt.packet_id(),
packet_name: pkt.packet_name().to_string(),
});
Ok(pkt)
}
}

View file

@ -1,23 +1,24 @@
//! Put stuff in here if you find that you have to write the same code for
//! multiple playgrounds.
use valence::client::event::StartSneaking;
use valence::client::command::{SneakState, Sneaking};
use valence::prelude::*;
/// Toggles client's game mode between survival and creative when they start
/// sneaking.
pub fn toggle_gamemode_on_sneak(
mut clients: Query<&mut GameMode>,
mut events: EventReader<StartSneaking>,
mut events: EventReader<Sneaking>,
) {
for event in events.iter() {
let Ok(mut mode) = clients.get_component_mut::<GameMode>(event.client) else {
continue;
};
*mode = match *mode {
GameMode::Survival => GameMode::Creative,
GameMode::Creative => GameMode::Survival,
_ => GameMode::Creative,
};
if event.state == SneakState::Start {
if let Ok(mut mode) = clients.get_mut(event.client) {
*mode = match *mode {
GameMode::Survival => GameMode::Creative,
GameMode::Creative => GameMode::Survival,
_ => GameMode::Creative,
};
}
}
}
}

View file

@ -1,3 +1,4 @@
use tracing::Level;
use valence::bevy_app::App;
#[allow(dead_code)]
@ -5,7 +6,9 @@ mod extras;
mod playground;
fn main() {
tracing_subscriber::fmt().init();
tracing_subscriber::fmt()
.with_max_level(Level::DEBUG)
.init();
let mut app = App::new();
playground::build_app(&mut app);

View file

@ -1,4 +1,4 @@
use valence::client::{default_event_handler, despawn_disconnected_clients};
use valence::client::despawn_disconnected_clients;
use valence::prelude::*;
#[allow(unused_imports)]
@ -9,7 +9,6 @@ const SPAWN_Y: i32 = 64;
pub fn build_app(app: &mut App) {
app.add_plugin(ServerPlugin::new(()).with_connection_mode(ConnectionMode::Offline))
.add_startup_system(setup)
.add_system(default_event_handler.in_schedule(EventLoopSchedule))
.add_system(init_clients)
.add_system(despawn_disconnected_clients)
.add_system(toggle_gamemode_on_sneak.in_schedule(EventLoopSchedule));
@ -39,13 +38,13 @@ fn setup(
}
fn init_clients(
mut clients: Query<(&mut Position, &mut Location), Added<Client>>,
mut clients: Query<(&mut Location, &mut Position), Added<Client>>,
instances: Query<Entity, With<Instance>>,
) {
for (mut pos, mut loc) in &mut clients {
pos.0 = [0.5, SPAWN_Y as f64 + 1.0, 0.5].into();
for (mut loc, mut pos) in &mut clients {
loc.0 = instances.single();
pos.set([0.5, SPAWN_Y as f64 + 1.0, 0.5]);
}
}
// Add new systems here!
// Add more systems here!

View file

@ -2,8 +2,6 @@
use std::time::Instant;
use valence::client::{default_event_handler, despawn_disconnected_clients};
use valence::entity::player::PlayerEntityBundle;
use valence::instance::{Chunk, Instance};
use valence::prelude::*;
@ -24,7 +22,6 @@ fn main() {
)
.add_startup_system(setup)
.add_systems((
default_event_handler.in_schedule(EventLoopSchedule),
record_tick_start_time.in_base_set(CoreSet::First),
print_tick_time.in_base_set(CoreSet::Last),
init_clients,
@ -72,18 +69,12 @@ fn setup(
}
fn init_clients(
mut clients: Query<(Entity, &UniqueId, &mut GameMode), Added<Client>>,
mut clients: Query<(&mut Location, &mut Position, &mut GameMode), Added<Client>>,
instances: Query<Entity, With<Instance>>,
mut commands: Commands,
) {
for (entity, uuid, mut game_mode) in &mut clients {
for (mut loc, mut pos, mut game_mode) in &mut clients {
loc.0 = instances.single();
pos.set([0.0, SPAWN_Y as f64 + 1.0, 0.0]);
*game_mode = GameMode::Creative;
commands.entity(entity).insert(PlayerEntityBundle {
location: Location(instances.single()),
position: Position::new([0.0, SPAWN_Y as f64 + 1.0, 0.0]),
uuid: *uuid,
..Default::default()
});
}
}

View file

@ -1,6 +1,5 @@
#![allow(clippy::type_complexity)]
use valence::client::{default_event_handler, despawn_disconnected_clients};
use valence::prelude::*;
const SPAWN_Y: i32 = 0;
@ -11,11 +10,7 @@ pub fn main() {
App::new()
.add_plugin(ServerPlugin::new(()))
.add_startup_system(setup)
.add_systems((
default_event_handler.in_schedule(EventLoopSchedule),
init_clients,
despawn_disconnected_clients,
))
.add_systems((init_clients, despawn_disconnected_clients))
.add_systems(PlayerList::default_systems())
.run();
}

View file

@ -1,8 +1,6 @@
#![allow(clippy::type_complexity)]
use valence::client::event::{ChatMessage, PlayerInteractBlock};
use valence::client::{default_event_handler, despawn_disconnected_clients};
use valence::entity::player::PlayerEntityBundle;
use valence::client::misc::{ChatMessage, InteractBlock};
use valence::nbt::{compound, List};
use valence::prelude::*;
use valence::protocol::types::Hand;
@ -17,12 +15,7 @@ pub fn main() {
App::new()
.add_plugin(ServerPlugin::new(()))
.add_startup_system(setup)
.add_systems((
default_event_handler.in_schedule(EventLoopSchedule),
event_handler.in_schedule(EventLoopSchedule),
init_clients,
despawn_disconnected_clients,
))
.add_systems((event_handler, init_clients, despawn_disconnected_clients))
.add_systems(PlayerList::default_systems())
.run();
}
@ -69,27 +62,22 @@ fn setup(
}
fn init_clients(
mut clients: Query<(Entity, &UniqueId, &mut GameMode), Added<Client>>,
mut clients: Query<(&mut Location, &mut Position, &mut Look, &mut GameMode), Added<Client>>,
instances: Query<Entity, With<Instance>>,
mut commands: Commands,
) {
for (entity, uuid, mut game_mode) in &mut clients {
*game_mode = GameMode::Creative;
for (mut loc, mut pos, mut look, mut game_mode) in &mut clients {
loc.0 = instances.single();
pos.set([1.5, FLOOR_Y as f64 + 1.0, 1.5]);
*look = Look::new(-90.0, 0.0);
commands.entity(entity).insert(PlayerEntityBundle {
location: Location(instances.single()),
position: Position::new([1.5, FLOOR_Y as f64 + 1.0, 1.5]),
look: Look::new(-90.0, 0.0),
uuid: *uuid,
..Default::default()
});
*game_mode = GameMode::Creative;
}
}
fn event_handler(
clients: Query<(&Username, &Properties, &UniqueId)>,
mut messages: EventReader<ChatMessage>,
mut block_interacts: EventReader<PlayerInteractBlock>,
mut block_interacts: EventReader<InteractBlock>,
mut instances: Query<&mut Instance>,
) {
let mut instance = instances.single_mut();
@ -107,7 +95,7 @@ fn event_handler(
nbt.insert("Text3", format!("~{}", username).italic());
}
for PlayerInteractBlock {
for InteractBlock {
client,
position,
hand,

View file

@ -1,8 +1,7 @@
#![allow(clippy::type_complexity)]
use valence::client::event::{PlayerInteractBlock, StartDigging, StartSneaking, StopDestroyBlock};
use valence::client::{default_event_handler, despawn_disconnected_clients};
use valence::entity::player::PlayerEntityBundle;
use valence::client::misc::InteractBlock;
use valence::client::ClientInventoryState;
use valence::prelude::*;
use valence::protocol::types::Hand;
@ -16,16 +15,12 @@ pub fn main() {
.add_startup_system(setup)
.add_system(init_clients)
.add_system(despawn_disconnected_clients)
.add_systems(
(
default_event_handler,
toggle_gamemode_on_sneak,
digging_creative_mode,
digging_survival_mode,
place_blocks,
)
.in_schedule(EventLoopSchedule),
)
.add_systems((
toggle_gamemode_on_sneak,
digging_creative_mode,
digging_survival_mode,
place_blocks,
))
.add_systems(PlayerList::default_systems())
.run();
}
@ -54,43 +49,37 @@ fn setup(
}
fn init_clients(
mut clients: Query<(Entity, &UniqueId, &mut Client, &mut GameMode), Added<Client>>,
mut clients: Query<(&mut Client, &mut Location, &mut Position, &mut GameMode), Added<Client>>,
instances: Query<Entity, With<Instance>>,
mut commands: Commands,
) {
for (entity, uuid, mut client, mut game_mode) in &mut clients {
for (mut client, mut loc, mut pos, mut game_mode) in &mut clients {
*game_mode = GameMode::Creative;
client.send_message("Welcome to Valence! Build something cool.".italic());
loc.0 = instances.single();
pos.set([0.0, SPAWN_Y as f64 + 1.0, 0.0]);
commands.entity(entity).insert(PlayerEntityBundle {
location: Location(instances.single()),
position: Position::new([0.0, SPAWN_Y as f64 + 1.0, 0.0]),
uuid: *uuid,
..Default::default()
});
client.send_message("Welcome to Valence! Build something cool.".italic());
}
}
fn toggle_gamemode_on_sneak(
mut clients: Query<&mut GameMode>,
mut events: EventReader<StartSneaking>,
) {
fn toggle_gamemode_on_sneak(mut clients: Query<&mut GameMode>, mut events: EventReader<Sneaking>) {
for event in events.iter() {
let Ok(mut mode) = clients.get_mut(event.client) else {
continue;
};
*mode = match *mode {
GameMode::Survival => GameMode::Creative,
GameMode::Creative => GameMode::Survival,
_ => GameMode::Creative,
};
if event.state == SneakState::Start {
*mode = match *mode {
GameMode::Survival => GameMode::Creative,
GameMode::Creative => GameMode::Survival,
_ => GameMode::Creative,
};
}
}
}
fn digging_creative_mode(
clients: Query<&GameMode>,
mut instances: Query<&mut Instance>,
mut events: EventReader<StartDigging>,
mut events: EventReader<Digging>,
) {
let mut instance = instances.single_mut();
@ -98,7 +87,7 @@ fn digging_creative_mode(
let Ok(game_mode) = clients.get(event.client) else {
continue;
};
if *game_mode == GameMode::Creative {
if *game_mode == GameMode::Creative && event.state == DiggingState::Start {
instance.set_block(event.position, BlockState::AIR);
}
}
@ -107,7 +96,7 @@ fn digging_creative_mode(
fn digging_survival_mode(
clients: Query<&GameMode>,
mut instances: Query<&mut Instance>,
mut events: EventReader<StopDestroyBlock>,
mut events: EventReader<Digging>,
) {
let mut instance = instances.single_mut();
@ -115,16 +104,16 @@ fn digging_survival_mode(
let Ok(game_mode) = clients.get(event.client) else {
continue;
};
if *game_mode == GameMode::Survival {
if *game_mode == GameMode::Survival && event.state == DiggingState::Stop {
instance.set_block(event.position, BlockState::AIR);
}
}
}
fn place_blocks(
mut clients: Query<(&mut Inventory, &GameMode, &PlayerInventoryState)>,
mut clients: Query<(&mut Inventory, &GameMode, &ClientInventoryState)>,
mut instances: Query<&mut Instance>,
mut events: EventReader<PlayerInteractBlock>,
mut events: EventReader<InteractBlock>,
) {
let mut instance = instances.single_mut();
@ -158,7 +147,7 @@ fn place_blocks(
inventory.set_slot(slot_id, None);
}
}
let real_pos = event.position.get_in_direction(event.direction);
let real_pos = event.position.get_in_direction(event.face);
instance.set_block(real_pos, block_kind.to_state());
}
}

View file

@ -1,9 +1,6 @@
#![allow(clippy::type_complexity)]
use tracing::warn;
use valence::client::event::{PlayerInteractBlock, StartSneaking};
use valence::client::{default_event_handler, despawn_disconnected_clients};
use valence::entity::player::PlayerEntityBundle;
use valence::client::misc::InteractBlock;
use valence::prelude::*;
const SPAWN_Y: i32 = 64;
@ -16,10 +13,7 @@ pub fn main() {
.add_plugin(ServerPlugin::new(()))
.add_startup_system(setup)
.add_system(init_clients)
.add_systems(
(default_event_handler, toggle_gamemode_on_sneak, open_chest)
.in_schedule(EventLoopSchedule),
)
.add_systems((toggle_gamemode_on_sneak, open_chest))
.add_systems(PlayerList::default_systems())
.add_system(despawn_disconnected_clients)
.run();
@ -56,53 +50,42 @@ fn setup(
}
fn init_clients(
mut clients: Query<(Entity, &UniqueId, &mut GameMode), Added<Client>>,
mut clients: Query<(&mut Location, &mut Position, &mut GameMode), Added<Client>>,
instances: Query<Entity, With<Instance>>,
mut commands: Commands,
) {
for (entity, uuid, mut game_mode) in &mut clients {
for (mut loc, mut pos, mut game_mode) in &mut clients {
loc.0 = instances.single();
pos.set([0.5, SPAWN_Y as f64 + 1.0, 0.5]);
*game_mode = GameMode::Creative;
commands.entity(entity).insert(PlayerEntityBundle {
location: Location(instances.single()),
position: Position::new([0.5, SPAWN_Y as f64 + 1.0, 0.5]),
uuid: *uuid,
..Default::default()
});
}
}
fn toggle_gamemode_on_sneak(
mut clients: Query<&mut GameMode>,
mut events: EventReader<StartSneaking>,
) {
fn toggle_gamemode_on_sneak(mut clients: Query<&mut GameMode>, mut events: EventReader<Sneaking>) {
for event in events.iter() {
let Ok(mut mode) = clients.get_mut(event.client) else {
continue;
};
*mode = match *mode {
GameMode::Survival => GameMode::Creative,
GameMode::Creative => GameMode::Survival,
_ => GameMode::Creative,
};
if event.state == SneakState::Start {
*mode = match *mode {
GameMode::Survival => GameMode::Creative,
GameMode::Creative => GameMode::Survival,
_ => GameMode::Creative,
};
}
}
}
fn open_chest(
mut commands: Commands,
inventories: Query<Entity, (With<Inventory>, Without<Client>)>,
mut events: EventReader<PlayerInteractBlock>,
mut events: EventReader<InteractBlock>,
) {
let Ok(inventory) = inventories.get_single() else {
warn!("No inventories");
return;
};
for event in events.iter() {
if event.position != CHEST_POS.into() {
continue;
}
let open_inventory = OpenInventory::new(inventory);
let open_inventory = OpenInventory::new(inventories.single());
commands.entity(event.client).insert(open_inventory);
}
}

View file

@ -2,9 +2,6 @@
use bevy_ecs::query::WorldQuery;
use glam::Vec3Swizzles;
use valence::client::event::{PlayerInteractEntity, StartSprinting, StopSprinting};
use valence::client::{default_event_handler, despawn_disconnected_clients};
use valence::entity::player::PlayerEntityBundle;
use valence::entity::EntityStatuses;
use valence::prelude::*;
@ -26,7 +23,7 @@ pub fn main() {
.add_plugin(ServerPlugin::new(()))
.add_startup_system(setup)
.add_system(init_clients)
.add_systems((default_event_handler, handle_combat_events).in_schedule(EventLoopSchedule))
.add_system(handle_combat_events.in_schedule(EventLoopSchedule))
.add_systems(PlayerList::default_systems())
.add_system(despawn_disconnected_clients)
.add_system(teleport_oob_clients)
@ -72,23 +69,18 @@ fn setup(
}
fn init_clients(
mut clients: Query<(Entity, &UniqueId), Added<Client>>,
mut clients: Query<(Entity, &mut Location, &mut Position), Added<Client>>,
instances: Query<Entity, With<Instance>>,
mut commands: Commands,
) {
for (entity, uuid) in &mut clients {
commands.entity(entity).insert((
CombatState {
last_attacked_tick: 0,
has_bonus_knockback: false,
},
PlayerEntityBundle {
location: Location(instances.single()),
position: Position::new([0.5, SPAWN_Y as f64, 0.5]),
uuid: *uuid,
..Default::default()
},
));
for (entity, mut loc, mut pos) in &mut clients {
loc.0 = instances.single();
pos.set([0.5, SPAWN_Y as f64, 0.5]);
commands.entity(entity).insert((CombatState {
last_attacked_tick: 0,
has_bonus_knockback: false,
},));
}
}
@ -102,36 +94,23 @@ struct CombatQuery {
}
fn handle_combat_events(
manager: Res<EntityManager>,
server: Res<Server>,
mut clients: Query<CombatQuery>,
mut start_sprinting: EventReader<StartSprinting>,
mut stop_sprinting: EventReader<StopSprinting>,
mut interact_with_entity: EventReader<PlayerInteractEntity>,
mut sprinting: EventReader<Sprinting>,
mut interact_entity: EventReader<InteractEntity>,
) {
for &StartSprinting { client } in start_sprinting.iter() {
for &Sprinting { client, state } in sprinting.iter() {
if let Ok(mut client) = clients.get_mut(client) {
client.state.has_bonus_knockback = true;
client.state.has_bonus_knockback = state == SprintState::Start;
}
}
for &StopSprinting { client } in stop_sprinting.iter() {
if let Ok(mut client) = clients.get_mut(client) {
client.state.has_bonus_knockback = false;
}
}
for &PlayerInteractEntity {
for &InteractEntity {
client: attacker_client,
entity_id,
entity: victim_client,
..
} in interact_with_entity.iter()
} in interact_entity.iter()
{
let Some(victim_client) = manager.get_with_id(entity_id) else {
// Attacked entity doesn't exist.
continue
};
let Ok([mut attacker, mut victim]) = clients.get_many_mut([attacker_client, victim_client]) else {
// Victim or attacker does not exist, or the attacker is attacking itself.
continue

View file

@ -2,9 +2,6 @@
use std::mem;
use valence::client::event::{StartDigging, StartSneaking};
use valence::client::{default_event_handler, despawn_disconnected_clients};
use valence::entity::player::PlayerEntityBundle;
use valence::prelude::*;
const BOARD_MIN_X: i32 = -30;
@ -30,10 +27,10 @@ pub fn main() {
.add_startup_system(setup_biomes.before(setup))
.add_startup_system(setup)
.add_system(init_clients)
.add_systems((default_event_handler, toggle_cell_on_dig).in_schedule(EventLoopSchedule))
.add_systems(PlayerList::default_systems())
.add_systems((
despawn_disconnected_clients,
toggle_cell_on_dig,
update_board,
pause_on_crouch,
reset_oob_clients,
@ -78,13 +75,10 @@ fn setup(
}
fn init_clients(
mut clients: Query<(Entity, &UniqueId, &mut Client, &mut GameMode), Added<Client>>,
mut clients: Query<(&mut Client, &mut Location, &mut Position), Added<Client>>,
instances: Query<Entity, With<Instance>>,
mut commands: Commands,
) {
for (entity, uuid, mut client, mut game_mode) in &mut clients {
*game_mode = GameMode::Survival;
for (mut client, mut loc, mut pos) in &mut clients {
client.send_message("Welcome to Conway's game of life in Minecraft!".italic());
client.send_message(
"Sneak to toggle running the simulation and the left mouse button to bring blocks to \
@ -92,12 +86,8 @@ fn init_clients(
.italic(),
);
commands.entity(entity).insert(PlayerEntityBundle {
location: Location(instances.single()),
position: Position(SPAWN_POS),
uuid: *uuid,
..Default::default()
});
loc.0 = instances.single();
pos.set(SPAWN_POS);
}
}
@ -163,12 +153,14 @@ impl LifeBoard {
}
}
fn toggle_cell_on_dig(mut events: EventReader<StartDigging>, mut board: ResMut<LifeBoard>) {
fn toggle_cell_on_dig(mut events: EventReader<Digging>, mut board: ResMut<LifeBoard>) {
for event in events.iter() {
let (x, z) = (event.position.x, event.position.z);
if event.state == DiggingState::Start {
let (x, z) = (event.position.x, event.position.z);
let live = board.get(x, z);
board.set(x, z, !live);
let live = board.get(x, z);
board.set(x, z, !live);
}
}
}
@ -197,18 +189,20 @@ fn update_board(
}
fn pause_on_crouch(
mut events: EventReader<StartSneaking>,
mut events: EventReader<Sneaking>,
mut board: ResMut<LifeBoard>,
mut clients: Query<&mut Client>,
) {
for _ in events.iter() {
board.paused = !board.paused;
for event in events.iter() {
if event.state == SneakState::Start {
board.paused = !board.paused;
for mut client in clients.iter_mut() {
if board.paused {
client.set_action_bar("Paused".italic().color(Color::RED));
} else {
client.set_action_bar("Playing".italic().color(Color::GREEN));
for mut client in clients.iter_mut() {
if board.paused {
client.set_action_bar("Paused".italic().color(Color::RED));
} else {
client.set_action_bar("Playing".italic().color(Color::GREEN));
}
}
}
}

View file

@ -3,8 +3,6 @@
use std::f64::consts::TAU;
use glam::{DQuat, EulerRot};
use valence::client::{default_event_handler, despawn_disconnected_clients};
use valence::entity::player::PlayerEntityBundle;
use valence::prelude::*;
type SpherePartBundle = valence::entity::cow::CowEntityBundle;
@ -28,7 +26,6 @@ fn main() {
.add_plugin(ServerPlugin::new(()))
.add_startup_system(setup)
.add_system(init_clients)
.add_system(default_event_handler.in_schedule(EventLoopSchedule))
.add_systems(PlayerList::default_systems())
.add_system(update_sphere)
.add_system(despawn_disconnected_clients)
@ -65,23 +62,18 @@ fn setup(
}
fn init_clients(
mut clients: Query<(Entity, &UniqueId, &mut GameMode), Added<Client>>,
mut clients: Query<(&mut Location, &mut Position, &mut GameMode), Added<Client>>,
instances: Query<Entity, With<Instance>>,
mut commands: Commands,
) {
for (entity, uuid, mut game_mode) in &mut clients {
*game_mode = GameMode::Creative;
for (mut loc, mut pos, mut game_mode) in &mut clients {
loc.0 = instances.single();
pos.set([
SPAWN_POS.x as f64 + 0.5,
SPAWN_POS.y as f64 + 1.0,
SPAWN_POS.z as f64 + 0.5,
]);
commands.entity(entity).insert(PlayerEntityBundle {
location: Location(instances.single()),
position: Position::new([
SPAWN_POS.x as f64 + 0.5,
SPAWN_POS.y as f64 + 1.0,
SPAWN_POS.z as f64 + 0.5,
]),
uuid: *uuid,
..Default::default()
});
*game_mode = GameMode::Creative;
}
}

View file

@ -1,8 +1,6 @@
#![allow(clippy::type_complexity)]
use valence::client::event::{PerformRespawn, StartSneaking};
use valence::client::{default_event_handler, despawn_disconnected_clients};
use valence::entity::player::PlayerEntityBundle;
use valence::client::misc::Respawn;
use valence::prelude::*;
const SPAWN_Y: i32 = 64;
@ -13,10 +11,7 @@ pub fn main() {
App::new()
.add_plugin(ServerPlugin::new(()).with_connection_mode(ConnectionMode::Offline))
.add_startup_system(setup)
.add_system(init_clients)
.add_systems(
(default_event_handler, squat_and_die, necromancy).in_schedule(EventLoopSchedule),
)
.add_systems((init_clients, squat_and_die, necromancy))
.add_systems(PlayerList::default_systems())
.add_system(despawn_disconnected_clients)
.run();
@ -48,36 +43,41 @@ fn setup(
}
fn init_clients(
mut clients: Query<(Entity, &UniqueId, &mut Client, &mut HasRespawnScreen), Added<Client>>,
mut clients: Query<
(
&mut Client,
&mut Location,
&mut Position,
&mut HasRespawnScreen,
),
Added<Client>,
>,
instances: Query<Entity, With<Instance>>,
mut commands: Commands,
) {
for (entity, uuid, mut client, mut has_respawn_screen) in &mut clients {
for (mut client, mut loc, mut pos, mut has_respawn_screen) in &mut clients {
loc.0 = instances.iter().next().unwrap();
pos.set([0.0, SPAWN_Y as f64 + 1.0, 0.0]);
has_respawn_screen.0 = true;
client.send_message(
"Welcome to Valence! Sneak to die in the game (but not in real life).".italic(),
);
commands.entity(entity).insert(PlayerEntityBundle {
location: Location(instances.iter().next().unwrap()),
position: Position::new([0.0, SPAWN_Y as f64 + 1.0, 0.0]),
uuid: *uuid,
..Default::default()
});
}
}
fn squat_and_die(mut clients: Query<&mut Client>, mut events: EventReader<StartSneaking>) {
fn squat_and_die(mut clients: Query<&mut Client>, mut events: EventReader<Sneaking>) {
for event in events.iter() {
if let Ok(mut client) = clients.get_mut(event.client) {
client.kill(None, "Squatted too hard.");
if event.state == SneakState::Start {
if let Ok(mut client) = clients.get_mut(event.client) {
client.kill(None, "Squatted too hard.");
}
}
}
}
fn necromancy(
mut clients: Query<(&mut Position, &mut Look, &mut Location)>,
mut events: EventReader<PerformRespawn>,
mut events: EventReader<Respawn>,
instances: Query<Entity, With<Instance>>,
) {
for event in events.iter() {

View file

@ -5,8 +5,6 @@ use std::time::{SystemTime, UNIX_EPOCH};
use rand::seq::SliceRandom;
use rand::Rng;
use valence::client::{default_event_handler, despawn_disconnected_clients};
use valence::entity::player::PlayerEntityBundle;
use valence::prelude::*;
use valence::protocol::packet::s2c::play::TitleFadeS2c;
use valence::protocol::sound::Sound;
@ -31,7 +29,6 @@ pub fn main() {
App::new()
.add_plugin(ServerPlugin::new(()))
.add_system(init_clients)
.add_system(default_event_handler.in_schedule(EventLoopSchedule))
.add_systems(PlayerList::default_systems())
.add_systems((
reset_clients.after(init_clients),
@ -52,15 +49,26 @@ struct GameState {
}
fn init_clients(
mut clients: Query<(Entity, &mut Client, &UniqueId, &mut IsFlat, &mut GameMode), Added<Client>>,
mut clients: Query<
(
Entity,
&mut Client,
&mut Location,
&mut IsFlat,
&mut GameMode,
),
Added<Client>,
>,
server: Res<Server>,
dimensions: Query<&DimensionType>,
biomes: Query<&Biome>,
mut commands: Commands,
) {
for (entity, mut client, uuid, mut is_flat, mut game_mode) in clients.iter_mut() {
for (entity, mut client, mut loc, mut is_flat, mut game_mode) in clients.iter_mut() {
loc.0 = entity;
is_flat.0 = true;
*game_mode = GameMode::Adventure;
client.send_message("Welcome to epic infinite parkour game!".italic());
let state = GameState {
@ -73,13 +81,7 @@ fn init_clients(
let instance = Instance::new(ident!("overworld"), &dimensions, &biomes, &server);
let player = PlayerEntityBundle {
location: Location(entity),
uuid: *uuid,
..Default::default()
};
commands.entity(entity).insert((state, instance, player));
commands.entity(entity).insert((state, instance));
}
}

View file

@ -2,8 +2,6 @@
use std::fmt;
use valence::client::{default_event_handler, despawn_disconnected_clients};
use valence::entity::player::PlayerEntityBundle;
use valence::prelude::*;
const SPAWN_Y: i32 = 64;
@ -15,7 +13,6 @@ pub fn main() {
.add_plugin(ServerPlugin::new(()))
.add_startup_system(setup)
.add_system(init_clients)
.add_system(default_event_handler.in_schedule(EventLoopSchedule))
.add_systems(PlayerList::default_systems())
.add_system(despawn_disconnected_clients)
.add_system(manage_particles)
@ -47,19 +44,13 @@ fn setup(
}
fn init_clients(
mut clients: Query<(Entity, &UniqueId, &mut GameMode), Added<Client>>,
mut clients: Query<(&mut Location, &mut Position, &mut GameMode), Added<Client>>,
instances: Query<Entity, With<Instance>>,
mut commands: Commands,
) {
for (entity, uuid, mut game_mode) in &mut clients {
for (mut loc, mut pos, mut game_mode) in &mut clients {
loc.0 = instances.single();
pos.set([0.5, SPAWN_Y as f64 + 1.0, 0.5]);
*game_mode = GameMode::Creative;
commands.entity(entity).insert(PlayerEntityBundle {
location: Location(instances.single()),
position: Position::new([0.5, SPAWN_Y as f64 + 1.0, 0.5]),
uuid: *uuid,
..Default::default()
});
}
}

View file

@ -1,7 +1,6 @@
#![allow(clippy::type_complexity)]
use rand::Rng;
use valence::client::{default_event_handler, despawn_disconnected_clients};
use valence::player_list::Entry;
use valence::prelude::*;
@ -15,7 +14,6 @@ fn main() {
App::new()
.add_plugin(ServerPlugin::new(()))
.add_startup_system(setup)
.add_system(default_event_handler.in_schedule(EventLoopSchedule))
.add_systems(PlayerList::default_systems())
.add_systems((
init_clients,

View file

@ -1,7 +1,6 @@
#![allow(clippy::type_complexity)]
use valence::client::event::{PlayerInteractEntity, ResourcePackStatus, ResourcePackStatusChange};
use valence::client::{default_event_handler, despawn_disconnected_clients};
use valence::client::misc::{ResourcePackStatus, ResourcePackStatusChange};
use valence::entity::player::PlayerEntityBundle;
use valence::entity::sheep::SheepEntityBundle;
use valence::prelude::*;
@ -15,15 +14,7 @@ pub fn main() {
App::new()
.add_plugin(ServerPlugin::new(()))
.add_startup_system(setup)
.add_system(init_clients)
.add_systems(
(
default_event_handler,
prompt_on_punch,
on_resource_pack_status,
)
.in_schedule(EventLoopSchedule),
)
.add_systems((init_clients, prompt_on_punch, on_resource_pack_status))
.add_systems(PlayerList::default_systems())
.add_system(despawn_disconnected_clients)
.run();
@ -79,19 +70,18 @@ fn init_clients(
}
}
fn prompt_on_punch(mut clients: Query<&mut Client>, mut events: EventReader<PlayerInteractEntity>) {
fn prompt_on_punch(mut clients: Query<&mut Client>, mut events: EventReader<InteractEntity>) {
for event in events.iter() {
let Ok(mut client) = clients.get_mut(event.client) else {
continue;
if let Ok(mut client) = clients.get_mut(event.client) {
if event.interact == EntityInteraction::Attack {
client.set_resource_pack(
"https://github.com/valence-rs/valence/raw/main/assets/example_pack.zip",
"d7c6108849fb190ec2a49f2d38b7f1f897d9ce9f",
false,
None,
);
}
};
if event.interact == EntityInteraction::Attack {
client.set_resource_pack(
"https://github.com/valence-rs/valence/raw/main/assets/example_pack.zip",
"d7c6108849fb190ec2a49f2d38b7f1f897d9ce9f",
false,
None,
);
}
}
}
@ -100,22 +90,22 @@ fn on_resource_pack_status(
mut events: EventReader<ResourcePackStatusChange>,
) {
for event in events.iter() {
let Ok(mut client) = clients.get_mut(event.client) else {
continue;
if let Ok(mut client) = clients.get_mut(event.client) {
match event.status {
ResourcePackStatus::Accepted => {
client.send_message("Resource pack accepted.".color(Color::GREEN));
}
ResourcePackStatus::Declined => {
client.send_message("Resource pack declined.".color(Color::RED));
}
ResourcePackStatus::FailedDownload => {
client.send_message("Resource pack failed to download.".color(Color::RED));
}
ResourcePackStatus::Loaded => {
client
.send_message("Resource pack successfully downloaded.".color(Color::BLUE));
}
}
};
match event.status {
ResourcePackStatus::Accepted => {
client.send_message("Resource pack accepted.".color(Color::GREEN));
}
ResourcePackStatus::Declined => {
client.send_message("Resource pack declined.".color(Color::RED));
}
ResourcePackStatus::FailedDownload => {
client.send_message("Resource pack failed to download.".color(Color::RED));
}
ResourcePackStatus::Loaded => {
client.send_message("Resource pack successfully downloaded.".color(Color::BLUE));
}
}
}
}

View file

@ -9,8 +9,6 @@ use std::time::SystemTime;
use flume::{Receiver, Sender};
use noise::{NoiseFn, SuperSimplex};
use tracing::info;
use valence::client::{default_event_handler, despawn_disconnected_clients};
use valence::entity::player::PlayerEntityBundle;
use valence::prelude::*;
const SPAWN_POS: DVec3 = DVec3::new(0.0, 200.0, 0.0);
@ -46,7 +44,6 @@ pub fn main() {
App::new()
.add_plugin(ServerPlugin::new(()))
.add_startup_system(setup)
.add_system(default_event_handler.in_schedule(EventLoopSchedule))
.add_systems(
(
init_clients,
@ -113,20 +110,14 @@ fn setup(
}
fn init_clients(
mut clients: Query<(Entity, &UniqueId, &mut IsFlat, &mut GameMode), Added<Client>>,
mut clients: Query<(&mut Location, &mut Position, &mut IsFlat, &mut GameMode), Added<Client>>,
instances: Query<Entity, With<Instance>>,
mut commands: Commands,
) {
for (entity, uuid, mut is_flat, mut game_mode) in &mut clients {
for (mut loc, mut pos, mut is_flat, mut game_mode) in &mut clients {
loc.0 = instances.single();
pos.set(SPAWN_POS);
is_flat.0 = true;
*game_mode = GameMode::Creative;
commands.entity(entity).insert(PlayerEntityBundle {
location: Location(instances.single()),
position: Position(SPAWN_POS),
uuid: *uuid,
..Default::default()
});
}
}

View file

@ -1,6 +1,5 @@
#![allow(clippy::type_complexity)]
use valence::client::{default_event_handler, despawn_disconnected_clients};
use valence::prelude::*;
use valence::protocol::translation_key;
@ -13,7 +12,6 @@ pub fn main() {
.add_plugin(ServerPlugin::new(()))
.add_startup_system(setup)
.add_system(init_clients)
.add_system(default_event_handler.in_schedule(EventLoopSchedule))
.add_systems(PlayerList::default_systems())
.add_system(despawn_disconnected_clients)
.run();

View file

@ -3,17 +3,17 @@ use std::net::IpAddr;
use std::num::Wrapping;
use std::time::Instant;
use bevy_app::{CoreSet, Plugin};
use bevy_app::prelude::*;
use bevy_ecs::prelude::*;
use bevy_ecs::query::WorldQuery;
use bevy_ecs::system::Command;
use bytes::BytesMut;
use bytes::{Bytes, BytesMut};
use glam::{DVec3, Vec3};
use rand::Rng;
use tracing::warn;
use valence_protocol::block_pos::BlockPos;
use valence_protocol::byte_angle::ByteAngle;
use valence_protocol::codec::{PacketDecoder, PacketEncoder};
use valence_protocol::encoder::PacketEncoder;
use valence_protocol::ident::Ident;
use valence_protocol::item::ItemStack;
use valence_protocol::packet::s2c::play::game_state_change::GameEventKind;
@ -36,50 +36,96 @@ use valence_protocol::Packet;
use crate::biome::BiomeRegistry;
use crate::component::{
Despawned, GameMode, Location, Look, OldLocation, OldPosition, OnGround, Ping, Position,
Properties, UniqueId, Username,
Properties, ScratchBuf, UniqueId, Username,
};
use crate::entity::player::PlayerEntityBundle;
use crate::entity::{
EntityId, EntityKind, EntityStatus, HeadYaw, ObjectData, PacketByteRange, TrackedData, Velocity,
};
use crate::instance::{Instance, WriteUpdatePacketsToInstancesSet};
use crate::inventory::{Inventory, InventoryKind};
use crate::packet::WritePacket;
use crate::prelude::ScratchBuf;
use crate::registry_codec::{RegistryCodec, RegistryCodecSet};
use crate::server::{NewClientInfo, Server};
use crate::util::velocity_to_packet_units;
use crate::view::{ChunkPos, ChunkView};
mod default_event_handler;
pub mod event;
pub mod action;
pub mod command;
pub mod interact_entity;
pub mod keepalive;
pub mod misc;
pub mod movement;
pub mod settings;
pub mod teleport;
pub use default_event_handler::*;
pub(crate) struct ClientPlugin;
/// When clients have their packet buffer flushed. Any system that writes
/// packets to clients should happen before this. Otherwise, the data
/// will arrive one tick late.
#[derive(SystemSet, Copy, Clone, PartialEq, Eq, Hash, Debug)]
pub struct FlushPacketsSet;
impl Plugin for ClientPlugin {
fn build(&self, app: &mut App) {
app.add_systems(
(
initial_join.after(RegistryCodecSet),
update_chunk_load_dist,
read_data_in_old_view
.after(WriteUpdatePacketsToInstancesSet)
.after(update_chunk_load_dist),
update_view.after(initial_join).after(read_data_in_old_view),
respawn.after(update_view),
remove_entities.after(update_view),
update_spawn_position.after(update_view),
update_old_view_dist.after(update_view),
update_game_mode,
update_tracked_data.after(WriteUpdatePacketsToInstancesSet),
init_tracked_data.after(WriteUpdatePacketsToInstancesSet),
update_op_level,
)
.in_base_set(CoreSet::PostUpdate)
.before(FlushPacketsSet),
)
.configure_set(
FlushPacketsSet
.in_base_set(CoreSet::PostUpdate)
.after(WriteUpdatePacketsToInstancesSet),
)
.add_system(flush_packets.in_set(FlushPacketsSet));
movement::build(app);
command::build(app);
keepalive::build(app);
interact_entity::build(app);
settings::build(app);
misc::build(app);
action::build(app);
teleport::build(app);
}
}
/// The bundle of components needed for clients to function. All components are
/// required unless otherwise stated.
#[derive(Bundle)]
pub(crate) struct ClientBundle {
client: Client,
settings: settings::ClientSettings,
scratch: ScratchBuf,
entity_remove_buffer: EntityRemoveBuf,
username: Username,
uuid: UniqueId,
ip: Ip,
properties: Properties,
location: Location,
old_location: OldLocation,
position: Position,
old_position: OldPosition,
look: Look,
on_ground: OnGround,
compass_pos: CompassPos,
game_mode: GameMode,
op_level: OpLevel,
player_action_sequence: PlayerActionSequence,
action_sequence: action::ActionSequence,
view_distance: ViewDistance,
old_view_distance: OldViewDistance,
death_location: DeathLocation,
keepalive_state: KeepaliveState,
keepalive_state: keepalive::KeepaliveState,
ping: Ping,
is_hardcore: IsHardcore,
prev_game_mode: PrevGameMode,
@ -88,10 +134,11 @@ pub(crate) struct ClientBundle {
has_respawn_screen: HasRespawnScreen,
is_debug: IsDebug,
is_flat: IsFlat,
teleport_state: TeleportState,
teleport_state: teleport::TeleportState,
cursor_item: CursorItem,
player_inventory_state: PlayerInventoryState,
player_inventory_state: ClientInventoryState,
inventory: Inventory,
player: PlayerEntityBundle,
}
impl ClientBundle {
@ -99,62 +146,39 @@ impl ClientBundle {
info: NewClientInfo,
conn: Box<dyn ClientConnection>,
enc: PacketEncoder,
dec: PacketDecoder,
) -> Self {
Self {
client: Client { conn, enc, dec },
client: Client { conn, enc },
settings: settings::ClientSettings::default(),
scratch: ScratchBuf::default(),
entity_remove_buffer: EntityRemoveBuf(vec![]),
username: Username(info.username),
uuid: UniqueId(info.uuid),
ip: Ip(info.ip),
properties: Properties(info.properties),
location: Location::default(),
old_location: OldLocation::default(),
position: Position::default(),
old_position: OldPosition::default(),
look: Look::default(),
on_ground: OnGround::default(),
compass_pos: CompassPos::default(),
game_mode: GameMode::default(),
op_level: OpLevel::default(),
player_action_sequence: PlayerActionSequence(0),
action_sequence: action::ActionSequence::default(),
view_distance: ViewDistance::default(),
old_view_distance: OldViewDistance(2),
death_location: DeathLocation::default(),
keepalive_state: KeepaliveState {
got_keepalive: true,
last_keepalive_id: 0,
keepalive_sent_time: Instant::now(),
},
keepalive_state: keepalive::KeepaliveState::new(),
ping: Ping::default(),
teleport_state: TeleportState {
teleport_id_counter: 0,
pending_teleports: 0,
synced_pos: DVec3::ZERO,
synced_look: Look {
// Client starts facing north.
yaw: 180.0,
pitch: 0.0,
},
},
teleport_state: teleport::TeleportState::new(),
is_hardcore: IsHardcore::default(),
is_flat: IsFlat::default(),
has_respawn_screen: HasRespawnScreen::default(),
cursor_item: CursorItem::default(),
player_inventory_state: PlayerInventoryState {
window_id: 0,
state_id: Wrapping(0),
slots_changed: 0,
client_updated_cursor_item: false,
// First slot of the hotbar.
held_item_slot: 36,
},
player_inventory_state: ClientInventoryState::new(),
inventory: Inventory::new(InventoryKind::Player),
prev_game_mode: PrevGameMode::default(),
hashed_seed: HashedSeed::default(),
reduced_debug_info: ReducedDebugInfo::default(),
is_debug: IsDebug::default(),
player: PlayerEntityBundle {
uuid: UniqueId(info.uuid),
..Default::default()
},
}
}
}
@ -168,12 +192,34 @@ impl ClientBundle {
pub struct Client {
conn: Box<dyn ClientConnection>,
enc: PacketEncoder,
dec: PacketDecoder,
}
pub(crate) trait ClientConnection: Send + Sync + 'static {
/// Represents the bidirectional packet channel between the server and a client
/// in the "play" state.
pub trait ClientConnection: Send + Sync + 'static {
/// Sends encoded clientbound packet data. This function must not block and
/// the data should be sent as soon as possible.
fn try_send(&mut self, bytes: BytesMut) -> anyhow::Result<()>;
fn try_recv(&mut self) -> anyhow::Result<BytesMut>;
/// Receives the next pending serverbound packet. This must return
/// immediately without blocking.
fn try_recv(&mut self) -> anyhow::Result<Option<ReceivedPacket>>;
/// The number of pending packets waiting to be received via
/// [`Self::try_recv`].
fn len(&self) -> usize;
fn is_empty(&self) -> bool {
self.len() == 0
}
}
#[derive(Clone, Debug)]
pub struct ReceivedPacket {
/// The moment in time this packet arrived. This is _not_ the instant this
/// packet was returned from [`ClientConnection::try_recv`].
pub timestamp: Instant,
/// This packet's ID.
pub id: i32,
/// The content of the packet, excluding the leading varint packet ID.
pub data: Bytes,
}
impl Drop for Client {
@ -195,6 +241,14 @@ impl WritePacket for Client {
}
impl Client {
pub fn connection(&self) -> &dyn ClientConnection {
self.conn.as_ref()
}
pub fn connection_mut(&mut self) -> &mut dyn ClientConnection {
self.conn.as_mut()
}
/// Flushes the packet queue to the underlying connection.
///
/// This is called automatically at the end of the tick and when the client
@ -443,14 +497,16 @@ impl OpLevel {
}
}
#[derive(Component, Copy, Clone, PartialEq, Eq, Default, Debug)]
pub struct PlayerActionSequence(i32);
#[derive(Component, Clone, PartialEq, Eq, Debug)]
pub struct ViewDistance(u8);
impl ViewDistance {
pub fn new(dist: u8) -> Self {
let mut new = Self(0);
new.set(dist);
new
}
pub fn get(&self) -> u8 {
self.0
}
@ -512,13 +568,6 @@ impl OldViewItem<'_> {
#[derive(Component, Clone, PartialEq, Eq, Default, Debug)]
pub struct DeathLocation(pub Option<(Ident<String>, BlockPos)>);
#[derive(Component, Debug)]
pub struct KeepaliveState {
got_keepalive: bool,
last_keepalive_id: u64,
keepalive_sent_time: Instant,
}
#[derive(Component, Copy, Clone, PartialEq, Eq, Default, Debug)]
pub struct IsHardcore(pub bool);
@ -533,9 +582,15 @@ pub struct HashedSeed(pub u64);
#[derive(Component, Copy, Clone, PartialEq, Eq, Default, Debug)]
pub struct ReducedDebugInfo(pub bool);
#[derive(Component, Copy, Clone, PartialEq, Eq, Default, Debug)]
#[derive(Component, Copy, Clone, PartialEq, Eq, Debug)]
pub struct HasRespawnScreen(pub bool);
impl Default for HasRespawnScreen {
fn default() -> Self {
Self(true)
}
}
#[derive(Component, Copy, Clone, PartialEq, Eq, Default, Debug)]
pub struct IsDebug(pub bool);
@ -543,28 +598,6 @@ pub struct IsDebug(pub bool);
#[derive(Component, Copy, Clone, PartialEq, Eq, Default, Debug)]
pub struct IsFlat(pub bool);
#[derive(Component, Debug)]
pub struct TeleportState {
/// Counts up as teleports are made.
teleport_id_counter: u32,
/// The number of pending client teleports that have yet to receive a
/// confirmation. Inbound client position packets should be ignored while
/// this is nonzero.
pending_teleports: u32,
synced_pos: DVec3,
synced_look: Look,
}
impl TeleportState {
pub fn teleport_id_counter(&self) -> u32 {
self.teleport_id_counter
}
pub fn pending_teleports(&self) -> u32 {
self.pending_teleports
}
}
/// The item stack that the client thinks it's holding under the mouse
/// cursor.
#[derive(Component, Clone, PartialEq, Default, Debug)]
@ -573,7 +606,7 @@ pub struct CursorItem(pub Option<ItemStack>);
// TODO: move this component to inventory module?
/// Miscellaneous inventory data.
#[derive(Component, Debug)]
pub struct PlayerInventoryState {
pub struct ClientInventoryState {
/// The current window ID. Incremented when inventories are opened.
pub(crate) window_id: u8,
pub(crate) state_id: Wrapping<i32>,
@ -588,7 +621,18 @@ pub struct PlayerInventoryState {
pub(crate) held_item_slot: u16,
}
impl PlayerInventoryState {
impl ClientInventoryState {
fn new() -> Self {
Self {
window_id: 0,
state_id: Wrapping(0),
slots_changed: 0,
client_updated_cursor_item: false,
// First slot of the hotbar.
held_item_slot: 36,
}
}
pub fn held_item_slot(&self) -> u16 {
self.held_item_slot
}
@ -607,48 +651,6 @@ pub fn despawn_disconnected_clients(
}
}
pub(crate) struct ClientPlugin;
/// When clients have their packet buffer flushed. Any system that writes
/// packets to clients should happen before this. Otherwise, the data
/// will arrive one tick late.
#[derive(SystemSet, Copy, Clone, PartialEq, Eq, Hash, Debug)]
pub struct FlushPacketsSet;
impl Plugin for ClientPlugin {
fn build(&self, app: &mut bevy_app::App) {
app.add_systems(
(
initial_join.after(RegistryCodecSet),
update_chunk_load_dist,
read_data_in_old_view
.after(WriteUpdatePacketsToInstancesSet)
.after(update_chunk_load_dist),
update_view.after(initial_join).after(read_data_in_old_view),
respawn.after(update_view),
remove_entities.after(update_view),
update_spawn_position.after(update_view),
update_old_view_dist.after(update_view),
teleport.after(update_view),
update_game_mode,
send_keepalive,
update_tracked_data.after(WriteUpdatePacketsToInstancesSet),
init_tracked_data.after(WriteUpdatePacketsToInstancesSet),
update_op_level,
acknowledge_player_actions,
)
.in_base_set(CoreSet::PostUpdate)
.before(FlushPacketsSet),
)
.configure_set(
FlushPacketsSet
.in_base_set(CoreSet::PostUpdate)
.after(WriteUpdatePacketsToInstancesSet),
)
.add_system(flush_packets.in_set(FlushPacketsSet));
}
}
#[derive(WorldQuery)]
#[world_query(mutable)]
struct ClientJoinQuery {
@ -1139,46 +1141,6 @@ fn update_game_mode(mut clients: Query<(&mut Client, &GameMode), Changed<GameMod
}
}
/// Syncs the client's position and look with the server.
///
/// This should happen after chunks are loaded so the client doesn't fall though
/// the floor.
fn teleport(
mut clients: Query<
(&mut Client, &mut TeleportState, &Position, &Look),
Or<(Changed<Position>, Changed<Look>)>,
>,
) {
for (mut client, mut state, pos, look) in &mut clients {
let changed_pos = pos.0 != state.synced_pos;
let changed_yaw = look.yaw != state.synced_look.yaw;
let changed_pitch = look.pitch != state.synced_look.pitch;
if changed_pos || changed_yaw || changed_pitch {
state.synced_pos = pos.0;
state.synced_look = *look;
let flags = PlayerPositionLookFlags::new()
.with_x(!changed_pos)
.with_y(!changed_pos)
.with_z(!changed_pos)
.with_y_rot(!changed_yaw)
.with_x_rot(!changed_pitch);
client.write_packet(&PlayerPositionLookS2c {
position: if changed_pos { pos.0.into() } else { [0.0; 3] },
yaw: if changed_yaw { look.yaw } else { 0.0 },
pitch: if changed_pitch { look.pitch } else { 0.0 },
flags,
teleport_id: VarInt(state.teleport_id_counter as i32),
});
state.pending_teleports = state.pending_teleports.wrapping_add(1);
state.teleport_id_counter = state.teleport_id_counter.wrapping_add(1);
}
}
}
fn update_old_view_dist(
mut clients: Query<(&mut OldViewDistance, &ViewDistance), Changed<ViewDistance>>,
) {
@ -1243,44 +1205,6 @@ fn update_op_level(mut clients: Query<(&mut Client, &OpLevel), Changed<OpLevel>>
}
}
fn acknowledge_player_actions(
mut clients: Query<(&mut Client, &mut PlayerActionSequence), Changed<PlayerActionSequence>>,
) {
for (mut client, mut action_seq) in &mut clients {
if action_seq.0 != 0 {
client.write_packet(&PlayerActionResponseS2c {
sequence: VarInt(action_seq.0),
});
action_seq.0 = 0;
}
}
}
fn send_keepalive(
mut clients: Query<(Entity, &mut Client, &mut KeepaliveState)>,
server: Res<Server>,
mut commands: Commands,
) {
if server.current_tick() % (server.tps() * 10) == 0 {
let mut rng = rand::thread_rng();
for (entity, mut client, mut state) in &mut clients {
if state.got_keepalive {
let id = rng.gen();
client.write_packet(&KeepAliveS2c { id });
state.got_keepalive = false;
state.last_keepalive_id = id;
state.keepalive_sent_time = Instant::now();
} else {
warn!("Client {entity:?} timed out (no keepalive response)");
commands.entity(entity).remove::<Client>();
}
}
}
}
#[cfg(test)]
mod tests {
use std::collections::BTreeSet;
@ -1322,7 +1246,7 @@ mod tests {
let mut loaded_chunks = BTreeSet::new();
for pkt in client_helper.collect_sent().unwrap() {
for pkt in client_helper.collect_sent() {
if let S2cPlayPacket::ChunkDataS2c(ChunkDataS2c {
chunk_x, chunk_z, ..
}) = pkt
@ -1347,7 +1271,7 @@ mod tests {
app.update();
let client = app.world.entity_mut(client_ent);
for pkt in client_helper.collect_sent().unwrap() {
for pkt in client_helper.collect_sent() {
match pkt {
S2cPlayPacket::ChunkDataS2c(ChunkDataS2c {
chunk_x, chunk_z, ..

View file

@ -0,0 +1,105 @@
use valence_protocol::block_pos::BlockPos;
use valence_protocol::packet::c2s::play::player_action::Action;
use valence_protocol::packet::c2s::play::PlayerActionC2s;
use valence_protocol::types::Direction;
use super::*;
use crate::event_loop::{EventLoopSchedule, EventLoopSet, PacketEvent};
pub(super) fn build(app: &mut App) {
app.add_event::<Digging>()
.add_system(
handle_player_action
.in_schedule(EventLoopSchedule)
.in_base_set(EventLoopSet::PreUpdate),
)
.add_system(
acknowledge_player_actions
.in_base_set(CoreSet::PostUpdate)
.before(FlushPacketsSet),
);
}
#[derive(Copy, Clone, Debug)]
pub struct Digging {
pub client: Entity,
pub position: BlockPos,
pub direction: Direction,
pub state: DiggingState,
}
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub enum DiggingState {
Start,
Abort,
Stop,
}
#[derive(Component, Copy, Clone, PartialEq, Eq, Default, Debug)]
pub struct ActionSequence(i32);
impl ActionSequence {
pub fn update(&mut self, val: i32) {
self.0 = self.0.max(val);
}
pub fn get(&self) -> i32 {
self.0
}
}
fn handle_player_action(
mut clients: Query<&mut ActionSequence>,
mut packets: EventReader<PacketEvent>,
mut digging_events: EventWriter<Digging>,
) {
for packet in packets.iter() {
if let Some(pkt) = packet.decode::<PlayerActionC2s>() {
if let Ok(mut seq) = clients.get_mut(packet.client) {
seq.update(pkt.sequence.0);
}
// TODO: check that digging is happening within configurable distance to client.
// TODO: check that blocks are being broken at the appropriate speeds.
match pkt.action {
Action::StartDestroyBlock => digging_events.send(Digging {
client: packet.client,
position: pkt.position,
direction: pkt.direction,
state: DiggingState::Start,
}),
Action::AbortDestroyBlock => digging_events.send(Digging {
client: packet.client,
position: pkt.position,
direction: pkt.direction,
state: DiggingState::Abort,
}),
Action::StopDestroyBlock => digging_events.send(Digging {
client: packet.client,
position: pkt.position,
direction: pkt.direction,
state: DiggingState::Stop,
}),
Action::DropAllItems => {}
Action::DropItem => {}
Action::ReleaseUseItem => todo!(), // TODO: release use item.
Action::SwapItemWithOffhand => {}
}
}
}
}
fn acknowledge_player_actions(
mut clients: Query<(&mut Client, &mut ActionSequence), Changed<ActionSequence>>,
) {
for (mut client, mut action_seq) in &mut clients {
if action_seq.0 != 0 {
client.write_packet(&PlayerActionResponseS2c {
sequence: VarInt(action_seq.0),
});
action_seq.0 = 0;
}
}
}

View file

@ -0,0 +1,134 @@
use bevy_app::prelude::*;
use bevy_ecs::prelude::*;
use valence_protocol::packet::c2s::play::client_command::Action;
use valence_protocol::packet::c2s::play::ClientCommandC2s;
use crate::entity::entity::Flags;
use crate::event_loop::{EventLoopSchedule, EventLoopSet, PacketEvent};
pub(super) fn build(app: &mut App) {
app.add_event::<Sprinting>()
.add_event::<Sneaking>()
.add_event::<JumpWithHorse>()
.add_event::<LeaveBed>()
.add_system(
handle_client_command
.in_schedule(EventLoopSchedule)
.in_base_set(EventLoopSet::PreUpdate),
);
}
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub struct Sprinting {
pub client: Entity,
pub state: SprintState,
}
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub enum SprintState {
Start,
Stop,
}
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub struct Sneaking {
pub client: Entity,
pub state: SneakState,
}
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub enum SneakState {
Start,
Stop,
}
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub struct JumpWithHorse {
pub client: Entity,
pub state: JumpWithHorseState,
}
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub enum JumpWithHorseState {
Start {
/// The power of the horse jump in `0..=100`.
power: u8,
},
Stop,
}
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub struct LeaveBed {
pub client: Entity,
}
fn handle_client_command(
mut packets: EventReader<PacketEvent>,
mut clients: Query<&mut Flags>,
mut sprinting_events: EventWriter<Sprinting>,
mut sneaking_events: EventWriter<Sneaking>,
mut jump_with_horse_events: EventWriter<JumpWithHorse>,
mut leave_bed_events: EventWriter<LeaveBed>,
) {
for packet in packets.iter() {
if let Some(pkt) = packet.decode::<ClientCommandC2s>() {
match pkt.action {
Action::StartSneaking => {
if let Ok(mut flags) = clients.get_mut(packet.client) {
flags.set_sneaking(true);
}
sneaking_events.send(Sneaking {
client: packet.client,
state: SneakState::Start,
})
}
Action::StopSneaking => {
if let Ok(mut flags) = clients.get_mut(packet.client) {
flags.set_sneaking(false);
}
sneaking_events.send(Sneaking {
client: packet.client,
state: SneakState::Stop,
})
}
Action::LeaveBed => leave_bed_events.send(LeaveBed {
client: packet.client,
}),
Action::StartSprinting => {
if let Ok(mut flags) = clients.get_mut(packet.client) {
flags.set_sprinting(true);
}
sprinting_events.send(Sprinting {
client: packet.client,
state: SprintState::Start,
});
}
Action::StopSprinting => {
if let Ok(mut flags) = clients.get_mut(packet.client) {
flags.set_sprinting(false);
}
sprinting_events.send(Sprinting {
client: packet.client,
state: SprintState::Stop,
})
}
Action::StartJumpWithHorse => jump_with_horse_events.send(JumpWithHorse {
client: packet.client,
state: JumpWithHorseState::Start {
power: pkt.jump_boost.0 as u8,
},
}),
Action::StopJumpWithHorse => jump_with_horse_events.send(JumpWithHorse {
client: packet.client,
state: JumpWithHorseState::Stop,
}),
Action::OpenHorseInventory => {} // TODO
Action::StartFlyingWithElytra => {} // TODO
}
}
}
}

View file

@ -1,116 +0,0 @@
use bevy_ecs::prelude::*;
use bevy_ecs::query::WorldQuery;
use valence_protocol::types::Hand;
use super::event::{
ClientSettings, HandSwing, PlayerMove, StartSneaking, StartSprinting, StopSneaking,
StopSprinting,
};
use super::{Client, ViewDistance};
use crate::entity::player::PlayerModelParts;
use crate::entity::{entity, player, EntityAnimation, EntityAnimations, EntityKind, HeadYaw, Pose};
#[doc(hidden)]
#[derive(WorldQuery)]
#[world_query(mutable)]
pub struct DefaultEventHandlerQuery {
client: &'static mut Client,
view_dist: &'static mut ViewDistance,
head_yaw: &'static mut HeadYaw,
player_model_parts: Option<&'static mut PlayerModelParts>,
pose: &'static mut entity::Pose,
flags: &'static mut entity::Flags,
animations: Option<&'static mut EntityAnimations>,
entity_kind: Option<&'static EntityKind>,
main_arm: Option<&'static mut player::MainArm>,
}
/// The default event handler system which handles client events in a
/// reasonable default way.
///
/// For instance, movement events are handled by changing the entity's
/// position/rotation to match the received movement, crouching makes the
/// entity crouch, etc.
///
/// This system's primary purpose is to reduce boilerplate code in the
/// examples, but it can be used as a quick way to get started in your own
/// code. The precise behavior of this system is left unspecified and
/// is subject to change.
///
/// This system must be scheduled to run in the
/// [`EventLoopSchedule`](crate::client::event::EventLoopSchedule). Otherwise,
/// it may not function correctly.
#[allow(clippy::too_many_arguments)]
pub fn default_event_handler(
mut clients: Query<DefaultEventHandlerQuery>,
mut update_settings_events: EventReader<ClientSettings>,
mut player_move: EventReader<PlayerMove>,
mut start_sneaking: EventReader<StartSneaking>,
mut stop_sneaking: EventReader<StopSneaking>,
mut start_sprinting: EventReader<StartSprinting>,
mut stop_sprinting: EventReader<StopSprinting>,
mut swing_arm: EventReader<HandSwing>,
) {
for ClientSettings {
client,
view_distance,
displayed_skin_parts,
main_arm,
..
} in update_settings_events.iter()
{
if let Ok(mut q) = clients.get_mut(*client) {
q.view_dist.0 = *view_distance;
if let Some(mut parts) = q.player_model_parts {
parts.set_if_neq(PlayerModelParts(u8::from(*displayed_skin_parts) as i8));
}
if let Some(mut player_main_arm) = q.main_arm {
player_main_arm.0 = *main_arm as _;
}
}
}
for PlayerMove { client, yaw, .. } in player_move.iter() {
if let Ok(mut q) = clients.get_mut(*client) {
q.head_yaw.set_if_neq(HeadYaw(*yaw));
}
}
for StartSneaking { client } in start_sneaking.iter() {
if let Ok(mut q) = clients.get_mut(*client) {
q.pose.set_if_neq(entity::Pose(Pose::Sneaking));
}
}
for StopSneaking { client } in stop_sneaking.iter() {
if let Ok(mut q) = clients.get_mut(*client) {
q.pose.set_if_neq(entity::Pose(Pose::Standing));
}
}
for StartSprinting { client } in start_sprinting.iter() {
if let Ok(mut q) = clients.get_mut(*client) {
q.flags.set_sprinting(true);
}
}
for StopSprinting { client } in stop_sprinting.iter() {
if let Ok(mut q) = clients.get_mut(*client) {
q.flags.set_sprinting(false);
}
}
for HandSwing { client, hand } in swing_arm.iter() {
if let Ok(q) = clients.get_mut(*client) {
if let (Some(mut animations), Some(&EntityKind::PLAYER)) = (q.animations, q.entity_kind)
{
animations.trigger(match hand {
Hand::Main => EntityAnimation::SwingMainHand,
Hand::Off => EntityAnimation::SwingOffHand,
});
}
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,49 @@
use bevy_app::prelude::*;
use bevy_ecs::prelude::*;
use valence_protocol::packet::c2s::play::player_interact_entity::EntityInteraction;
use valence_protocol::packet::c2s::play::PlayerInteractEntityC2s;
use crate::entity::EntityManager;
use crate::event_loop::{EventLoopSchedule, EventLoopSet, PacketEvent};
pub(super) fn build(app: &mut App) {
app.add_event::<InteractEntity>().add_system(
handle_interact_entity
.in_schedule(EventLoopSchedule)
.in_base_set(EventLoopSet::PreUpdate),
);
}
#[derive(Copy, Clone, Debug)]
pub struct InteractEntity {
pub client: Entity,
/// The entity being interacted with.
pub entity: Entity,
/// If the client was sneaking during the interaction.
pub sneaking: bool,
/// The kind of interaction that occurred.
pub interact: EntityInteraction,
}
fn handle_interact_entity(
mut packets: EventReader<PacketEvent>,
entities: Res<EntityManager>,
mut events: EventWriter<InteractEntity>,
) {
for packet in packets.iter() {
if let Some(pkt) = packet.decode::<PlayerInteractEntityC2s>() {
// TODO: check that the entity is in the same instance as the player.
// TODO: check that the distance between the player and the interacted entity is
// within some configurable tolerance level.
if let Some(entity) = entities.get_by_id(pkt.entity_id.0) {
events.send(InteractEntity {
client: packet.client,
entity,
sneaking: pkt.sneaking,
interact: pkt.interact,
})
}
}
}
}

View file

@ -0,0 +1,85 @@
use valence_protocol::packet::c2s::play::KeepAliveC2s;
use super::*;
use crate::event_loop::{EventLoopSchedule, EventLoopSet, PacketEvent};
pub(super) fn build(app: &mut App) {
app.add_system(
send_keepalive
.in_base_set(CoreSet::PostUpdate)
.before(FlushPacketsSet),
)
.add_system(
handle_keepalive_response
.in_base_set(EventLoopSet::PreUpdate)
.in_schedule(EventLoopSchedule),
);
}
#[derive(Component, Debug)]
pub struct KeepaliveState {
got_keepalive: bool,
last_keepalive_id: u64,
keepalive_sent_time: Instant,
}
impl KeepaliveState {
pub(super) fn new() -> Self {
Self {
got_keepalive: true,
last_keepalive_id: 0,
keepalive_sent_time: Instant::now(),
}
}
}
fn send_keepalive(
mut clients: Query<(Entity, &mut Client, &mut KeepaliveState)>,
server: Res<Server>,
mut commands: Commands,
) {
if server.current_tick() % (server.tps() * 10) == 0 {
let mut rng = rand::thread_rng();
for (entity, mut client, mut state) in &mut clients {
if state.got_keepalive {
let id = rng.gen();
client.write_packet(&KeepAliveS2c { id });
state.got_keepalive = false;
state.last_keepalive_id = id;
// TODO: start timing when the packets are flushed.
state.keepalive_sent_time = Instant::now();
} else {
warn!("Client {entity:?} timed out (no keepalive response)");
commands.entity(entity).remove::<Client>();
}
}
}
}
fn handle_keepalive_response(
mut packets: EventReader<PacketEvent>,
mut clients: Query<(Entity, &mut KeepaliveState, &mut Ping)>,
mut commands: Commands,
) {
for packet in packets.iter() {
if let Some(pkt) = packet.decode::<KeepAliveC2s>() {
if let Ok((client, mut state, mut ping)) = clients.get_mut(packet.client) {
if state.got_keepalive {
warn!("unexpected keepalive from client {client:?}");
commands.entity(client).remove::<Client>();
} else if pkt.id != state.last_keepalive_id {
warn!(
"keepalive IDs don't match for client {client:?} (expected {}, got {})",
state.last_keepalive_id, pkt.id,
);
commands.entity(client).remove::<Client>();
} else {
state.got_keepalive = true;
ping.0 = state.keepalive_sent_time.elapsed().as_millis() as i32;
}
}
}
}
}

View file

@ -0,0 +1,150 @@
use bevy_app::prelude::*;
use bevy_ecs::prelude::*;
use glam::Vec3;
use valence_protocol::block_pos::BlockPos;
use valence_protocol::packet::c2s::play::{
ChatMessageC2s, ClientStatusC2s, HandSwingC2s, PlayerInteractBlockC2s, PlayerInteractItemC2s,
ResourcePackStatusC2s,
};
use valence_protocol::types::{Direction, Hand};
use super::action::ActionSequence;
use crate::event_loop::{EventLoopSchedule, EventLoopSet, PacketEvent};
pub(super) fn build(app: &mut App) {
app.add_event::<HandSwing>()
.add_event::<InteractBlock>()
.add_event::<ChatMessage>()
.add_event::<Respawn>()
.add_event::<RequestStats>()
.add_event::<ResourcePackStatusChange>()
.add_system(
handle_misc_packets
.in_schedule(EventLoopSchedule)
.in_base_set(EventLoopSet::PreUpdate),
);
}
#[derive(Copy, Clone, Debug)]
pub struct HandSwing {
pub client: Entity,
pub hand: Hand,
}
#[derive(Copy, Clone, Debug)]
pub struct InteractBlock {
pub client: Entity,
/// The hand that was used
pub hand: Hand,
/// The location of the block that was interacted with
pub position: BlockPos,
/// The face of the block that was clicked
pub face: Direction,
/// The position inside of the block that was clicked on
pub cursor_pos: Vec3,
/// Whether or not the player's head is inside a block
pub head_inside_block: bool,
/// Sequence number for synchronization
pub sequence: i32,
}
#[derive(Clone, Debug)]
pub struct ChatMessage {
pub client: Entity,
pub message: Box<str>,
pub timestamp: u64,
}
#[derive(Copy, Clone, Debug)]
pub struct Respawn {
pub client: Entity,
}
#[derive(Copy, Clone, Debug)]
pub struct RequestStats {
pub client: Entity,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum ResourcePackStatus {
/// The client has accepted the server's resource pack.
Accepted,
/// The client has declined the server's resource pack.
Declined,
/// The client has successfully loaded the server's resource pack.
Loaded,
/// The client has failed to download the server's resource pack.
FailedDownload,
}
#[derive(Clone, Debug)]
pub struct ResourcePackStatusChange {
pub client: Entity,
pub status: ResourcePackStatus,
}
#[allow(clippy::too_many_arguments)]
fn handle_misc_packets(
mut packets: EventReader<PacketEvent>,
mut clients: Query<&mut ActionSequence>,
mut hand_swing_events: EventWriter<HandSwing>,
mut interact_block_events: EventWriter<InteractBlock>,
mut chat_message_events: EventWriter<ChatMessage>,
mut respawn_events: EventWriter<Respawn>,
mut request_stats_events: EventWriter<RequestStats>,
mut resource_pack_status_change_events: EventWriter<ResourcePackStatusChange>,
) {
for packet in packets.iter() {
if let Some(pkt) = packet.decode::<HandSwingC2s>() {
hand_swing_events.send(HandSwing {
client: packet.client,
hand: pkt.hand,
});
} else if let Some(pkt) = packet.decode::<PlayerInteractBlockC2s>() {
if let Ok(mut action_seq) = clients.get_mut(packet.client) {
action_seq.update(pkt.sequence.0);
}
interact_block_events.send(InteractBlock {
client: packet.client,
hand: pkt.hand,
position: pkt.position,
face: pkt.face,
cursor_pos: pkt.cursor_pos.into(),
head_inside_block: pkt.head_inside_block,
sequence: pkt.sequence.0,
});
} else if let Some(pkt) = packet.decode::<PlayerInteractItemC2s>() {
if let Ok(mut action_seq) = clients.get_mut(packet.client) {
action_seq.update(pkt.sequence.0);
}
// TODO
} else if let Some(pkt) = packet.decode::<ChatMessageC2s>() {
chat_message_events.send(ChatMessage {
client: packet.client,
message: pkt.message.into(),
timestamp: pkt.timestamp,
});
} else if let Some(pkt) = packet.decode::<ClientStatusC2s>() {
match pkt {
ClientStatusC2s::PerformRespawn => respawn_events.send(Respawn {
client: packet.client,
}),
ClientStatusC2s::RequestStats => request_stats_events.send(RequestStats {
client: packet.client,
}),
}
} else if let Some(pkt) = packet.decode::<ResourcePackStatusC2s>() {
resource_pack_status_change_events.send(ResourcePackStatusChange {
client: packet.client,
status: match pkt {
ResourcePackStatusC2s::Accepted => ResourcePackStatus::Accepted,
ResourcePackStatusC2s::Declined => ResourcePackStatus::Declined,
ResourcePackStatusC2s::SuccessfullyLoaded => ResourcePackStatus::Loaded,
ResourcePackStatusC2s::FailedDownload => ResourcePackStatus::FailedDownload,
},
});
}
}
}

View file

@ -0,0 +1,210 @@
use bevy_app::prelude::*;
use bevy_ecs::prelude::*;
use glam::DVec3;
use valence_protocol::packet::c2s::play::{
Full, LookAndOnGround, OnGroundOnly, PositionAndOnGround, VehicleMoveC2s,
};
use super::teleport::TeleportState;
use crate::component::{Look, OnGround, Position};
use crate::entity::HeadYaw;
use crate::event_loop::{EventLoopSchedule, EventLoopSet, PacketEvent};
pub(super) fn build(app: &mut App) {
app.init_resource::<MovementSettings>()
.add_event::<Movement>()
.add_system(
handle_client_movement
.in_schedule(EventLoopSchedule)
.in_base_set(EventLoopSet::PreUpdate),
);
}
/// Configuration resource for client movement checks.
#[derive(Resource, Default)]
pub struct MovementSettings {
// TODO
}
/// Event sent when a client successfully moves.
#[derive(Clone, Debug)]
pub struct Movement {
pub client: Entity,
pub position: DVec3,
pub old_position: DVec3,
pub look: Look,
pub old_look: Look,
pub on_ground: bool,
pub old_on_ground: bool,
}
fn handle_client_movement(
mut packets: EventReader<PacketEvent>,
mut clients: Query<(
&mut Position,
&mut Look,
&mut HeadYaw,
&mut OnGround,
&mut TeleportState,
)>,
mut movement_events: EventWriter<Movement>,
) {
for packet in packets.iter() {
if let Some(pkt) = packet.decode::<PositionAndOnGround>() {
if let Ok((pos, look, head_yaw, on_ground, teleport_state)) =
clients.get_mut(packet.client)
{
let mov = Movement {
client: packet.client,
position: pkt.position.into(),
old_position: pos.0,
look: *look,
old_look: *look,
on_ground: pkt.on_ground,
old_on_ground: on_ground.0,
};
handle(
mov,
pos,
look,
head_yaw,
on_ground,
teleport_state,
&mut movement_events,
);
}
} else if let Some(pkt) = packet.decode::<Full>() {
if let Ok((pos, look, head_yaw, on_ground, teleport_state)) =
clients.get_mut(packet.client)
{
let mov = Movement {
client: packet.client,
position: pkt.position.into(),
old_position: pos.0,
look: Look {
yaw: pkt.yaw,
pitch: pkt.pitch,
},
old_look: *look,
on_ground: pkt.on_ground,
old_on_ground: on_ground.0,
};
handle(
mov,
pos,
look,
head_yaw,
on_ground,
teleport_state,
&mut movement_events,
);
}
} else if let Some(pkt) = packet.decode::<LookAndOnGround>() {
if let Ok((pos, look, head_yaw, on_ground, teleport_state)) =
clients.get_mut(packet.client)
{
let mov = Movement {
client: packet.client,
position: pos.0,
old_position: pos.0,
look: Look {
yaw: pkt.yaw,
pitch: pkt.pitch,
},
old_look: *look,
on_ground: pkt.on_ground,
old_on_ground: on_ground.0,
};
handle(
mov,
pos,
look,
head_yaw,
on_ground,
teleport_state,
&mut movement_events,
);
}
} else if let Some(pkt) = packet.decode::<OnGroundOnly>() {
if let Ok((pos, look, head_yaw, on_ground, teleport_state)) =
clients.get_mut(packet.client)
{
let mov = Movement {
client: packet.client,
position: pos.0,
old_position: pos.0,
look: *look,
old_look: *look,
on_ground: pkt.on_ground,
old_on_ground: on_ground.0,
};
handle(
mov,
pos,
look,
head_yaw,
on_ground,
teleport_state,
&mut movement_events,
);
}
} else if let Some(pkt) = packet.decode::<VehicleMoveC2s>() {
if let Ok((pos, look, head_yaw, on_ground, teleport_state)) =
clients.get_mut(packet.client)
{
let mov = Movement {
client: packet.client,
position: pkt.position.into(),
old_position: pos.0,
look: Look {
yaw: pkt.yaw,
pitch: pkt.pitch,
},
old_look: *look,
on_ground: on_ground.0,
old_on_ground: on_ground.0,
};
handle(
mov,
pos,
look,
head_yaw,
on_ground,
teleport_state,
&mut movement_events,
);
}
}
}
}
fn handle(
mov: Movement,
mut pos: Mut<Position>,
mut look: Mut<Look>,
mut head_yaw: Mut<HeadYaw>,
mut on_ground: Mut<OnGround>,
mut teleport_state: Mut<TeleportState>,
movement_events: &mut EventWriter<Movement>,
) {
if teleport_state.pending_teleports() != 0 {
return;
}
// TODO: check that the client isn't moving too fast / flying.
// TODO: check that the client isn't clipping through blocks.
pos.set_if_neq(Position(mov.position));
teleport_state.synced_pos = mov.position;
look.set_if_neq(mov.look);
teleport_state.synced_look = mov.look;
head_yaw.set_if_neq(HeadYaw(mov.look.yaw));
on_ground.set_if_neq(OnGround(mov.on_ground));
movement_events.send(mov);
}

View file

@ -0,0 +1,53 @@
pub use valence_protocol::packet::c2s::play::client_settings::ChatMode;
// use valence_protocol::packet::c2s::play::client_settings::MainArm;
use valence_protocol::packet::c2s::play::ClientSettingsC2s;
use super::*;
pub use crate::entity::player::{MainArm, PlayerModelParts};
use crate::event_loop::{EventLoopSchedule, EventLoopSet, PacketEvent};
pub(super) fn build(app: &mut App) {
app.add_system(
handle_client_settings
.in_schedule(EventLoopSchedule)
.in_base_set(EventLoopSet::PreUpdate),
);
}
#[derive(Component, Default, Debug)]
pub struct ClientSettings {
pub locale: Box<str>,
pub chat_mode: ChatMode,
pub chat_colors: bool,
pub enable_text_filtering: bool,
pub allow_server_listings: bool,
}
fn handle_client_settings(
mut packets: EventReader<PacketEvent>,
mut clients: Query<(
&mut ViewDistance,
&mut ClientSettings,
&mut PlayerModelParts,
&mut MainArm,
)>,
) {
for packet in packets.iter() {
if let Some(pkt) = packet.decode::<ClientSettingsC2s>() {
if let Ok((mut view_dist, mut settings, mut model_parts, mut main_arm)) =
clients.get_mut(packet.client)
{
view_dist.set_if_neq(ViewDistance::new(pkt.view_distance));
settings.locale = pkt.locale.into();
settings.chat_mode = pkt.chat_mode;
settings.chat_colors = pkt.chat_colors;
settings.enable_text_filtering = pkt.enable_text_filtering;
settings.allow_server_listings = pkt.allow_server_listings;
model_parts.set_if_neq(PlayerModelParts(u8::from(pkt.displayed_skin_parts) as i8));
main_arm.set_if_neq(MainArm(pkt.main_arm as i8));
}
}
}
}

View file

@ -0,0 +1,128 @@
use valence_protocol::packet::c2s::play::TeleportConfirmC2s;
use super::*;
use crate::event_loop::{EventLoopSchedule, EventLoopSet, PacketEvent};
pub(super) fn build(app: &mut App) {
app.add_system(
teleport
.after(update_view)
.before(FlushPacketsSet)
.in_base_set(CoreSet::PostUpdate),
)
.add_system(
handle_teleport_confirmations
.in_schedule(EventLoopSchedule)
.in_base_set(EventLoopSet::PreUpdate),
);
}
#[derive(Component, Debug)]
pub struct TeleportState {
/// Counts up as teleports are made.
teleport_id_counter: u32,
/// The number of pending client teleports that have yet to receive a
/// confirmation. Inbound client position packets should be ignored while
/// this is nonzero.
pending_teleports: u32,
pub(super) synced_pos: DVec3,
pub(super) synced_look: Look,
}
impl TeleportState {
pub(super) fn new() -> Self {
Self {
teleport_id_counter: 0,
pending_teleports: 0,
synced_pos: DVec3::ZERO,
synced_look: Look {
// Client starts facing north.
yaw: 180.0,
pitch: 0.0,
},
}
}
pub fn teleport_id_counter(&self) -> u32 {
self.teleport_id_counter
}
pub fn pending_teleports(&self) -> u32 {
self.pending_teleports
}
}
/// Syncs the client's position and look with the server.
///
/// This should happen after chunks are loaded so the client doesn't fall though
/// the floor.
fn teleport(
mut clients: Query<
(&mut Client, &mut TeleportState, &Position, &Look),
Or<(Changed<Position>, Changed<Look>)>,
>,
) {
for (mut client, mut state, pos, look) in &mut clients {
let changed_pos = pos.0 != state.synced_pos;
let changed_yaw = look.yaw != state.synced_look.yaw;
let changed_pitch = look.pitch != state.synced_look.pitch;
if changed_pos || changed_yaw || changed_pitch {
state.synced_pos = pos.0;
state.synced_look = *look;
let flags = PlayerPositionLookFlags::new()
.with_x(!changed_pos)
.with_y(!changed_pos)
.with_z(!changed_pos)
.with_y_rot(!changed_yaw)
.with_x_rot(!changed_pitch);
client.write_packet(&PlayerPositionLookS2c {
position: if changed_pos { pos.0.into() } else { [0.0; 3] },
yaw: if changed_yaw { look.yaw } else { 0.0 },
pitch: if changed_pitch { look.pitch } else { 0.0 },
flags,
teleport_id: VarInt(state.teleport_id_counter as i32),
});
state.pending_teleports = state.pending_teleports.wrapping_add(1);
state.teleport_id_counter = state.teleport_id_counter.wrapping_add(1);
}
}
}
fn handle_teleport_confirmations(
mut packets: EventReader<PacketEvent>,
mut clients: Query<&mut TeleportState>,
mut commands: Commands,
) {
for packet in packets.iter() {
if let Some(pkt) = packet.decode::<TeleportConfirmC2s>() {
if let Ok(mut state) = clients.get_mut(packet.client) {
if state.pending_teleports == 0 {
warn!(
"unexpected teleport confirmation from client {:?}",
packet.client
);
commands.entity(packet.client).remove::<Client>();
}
let got = pkt.teleport_id.0 as u32;
let expected = state
.teleport_id_counter
.wrapping_sub(state.pending_teleports);
if got == expected {
state.pending_teleports -= 1;
} else {
warn!(
"unexpected teleport ID for client {:?} (expected {expected}, got {got}",
packet.client
);
commands.entity(packet.client).remove::<Client>();
}
}
}
}
}

View file

@ -7,7 +7,7 @@ use glam::{DVec3, Vec3};
use uuid::Uuid;
use valence_protocol::types::{GameMode as ProtocolGameMode, Property};
use crate::prelude::FlushPacketsSet;
use crate::client::FlushPacketsSet;
use crate::util::{from_yaw_and_pitch, to_yaw_and_pitch};
use crate::view::ChunkPos;

View file

@ -437,12 +437,12 @@ impl EntityManager {
}
/// Gets the entity with the given entity ID.
pub fn get_with_id(&self, entity_id: i32) -> Option<Entity> {
pub fn get_by_id(&self, entity_id: i32) -> Option<Entity> {
self.id_to_entity.get(&entity_id).cloned()
}
/// Gets the entity with the given UUID.
pub fn get_with_uuid(&self, uuid: Uuid) -> Option<Entity> {
pub fn get_by_uuid(&self, uuid: Uuid) -> Option<Entity> {
self.uuid_to_entity.get(&uuid).cloned()
}
}

View file

@ -0,0 +1,177 @@
use std::time::Instant;
use bevy_app::prelude::*;
use bevy_ecs::prelude::*;
use bevy_ecs::schedule::ScheduleLabel;
use bevy_ecs::system::SystemState;
use bytes::Bytes;
use tracing::{debug, warn};
use valence_protocol::{Decode, Packet};
use crate::client::Client;
pub(crate) struct EventLoopPlugin;
impl Plugin for EventLoopPlugin {
fn build(&self, app: &mut bevy_app::App) {
app.configure_set(RunEventLoopSet.in_base_set(CoreSet::PreUpdate))
.add_system(run_event_loop.in_set(RunEventLoopSet))
.add_event::<PacketEvent>();
// Add the event loop schedule.
let mut event_loop = Schedule::new();
event_loop.set_default_base_set(EventLoopSet::Update);
event_loop.configure_sets((
EventLoopSet::PreUpdate.before(EventLoopSet::Update),
EventLoopSet::Update.before(EventLoopSet::PostUpdate),
EventLoopSet::PostUpdate,
));
app.add_schedule(EventLoopSchedule, event_loop);
}
}
/// The [`ScheduleLabel`] for the event loop [`Schedule`].
#[derive(ScheduleLabel, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Debug)]
pub struct EventLoopSchedule;
#[derive(SystemSet, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Debug)]
#[system_set(base)]
pub enum EventLoopSet {
PreUpdate,
#[default]
Update,
PostUpdate,
}
#[derive(SystemSet, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Debug)]
pub struct RunEventLoopSet;
#[derive(Clone, Debug)]
pub struct PacketEvent {
/// The client this packet originated from.
pub client: Entity,
/// The moment in time this packet arrived.
pub timestamp: Instant,
/// This packet's ID.
pub id: i32,
/// The content of the packet, excluding the leading varint packet ID.
pub data: Bytes,
}
impl PacketEvent {
/// Attempts to decode this packet as the packet `P`.
///
/// If the packet ID is mismatched or an error occurs, `None` is returned.
/// Otherwise, `Some` is returned containing the decoded packet.
#[inline]
pub fn decode<'a, P>(&'a self) -> Option<P>
where
P: Packet<'a> + Decode<'a>,
{
if self.id == P::PACKET_ID {
let mut r = &self.data[..];
match P::decode(&mut r) {
Ok(pkt) => {
if r.is_empty() {
return Some(pkt);
}
warn!(
"missed {} bytes while decoding packet {} (ID = {})",
r.len(),
pkt.packet_name(),
P::PACKET_ID
);
debug!("complete packet after partial decode: {pkt:?}");
}
Err(e) => {
warn!("failed to decode packet with ID of {}: {e:#}", P::PACKET_ID);
}
}
}
None
}
}
/// An exclusive system for running the event loop schedule.
pub(crate) fn run_event_loop(
world: &mut World,
state: &mut SystemState<(
Query<(Entity, &mut Client)>,
EventWriter<PacketEvent>,
Commands,
)>,
mut check_again: Local<Vec<(Entity, usize)>>,
) {
debug_assert!(check_again.is_empty());
let (mut clients, mut event_writer, mut commands) = state.get_mut(world);
for (entity, mut client) in &mut clients {
match client.connection_mut().try_recv() {
Ok(Some(pkt)) => {
event_writer.send(PacketEvent {
client: entity,
timestamp: pkt.timestamp,
id: pkt.id,
data: pkt.data,
});
let remaining = client.connection().len();
if remaining > 0 {
check_again.push((entity, remaining));
}
}
Ok(None) => {}
Err(e) => {
// Client is disconnected.
debug!("disconnecting client: {e:#}");
commands.entity(entity).remove::<Client>();
}
}
}
state.apply(world);
world.run_schedule(EventLoopSchedule);
while !check_again.is_empty() {
let (mut clients, mut event_writer, mut commands) = state.get_mut(world);
check_again.retain_mut(|(entity, remaining)| {
debug_assert!(*remaining > 0);
if let Ok((_, mut client)) = clients.get_mut(*entity) {
match client.connection_mut().try_recv() {
Ok(Some(pkt)) => {
event_writer.send(PacketEvent {
client: *entity,
timestamp: pkt.timestamp,
id: pkt.id,
data: pkt.data,
});
*remaining -= 1;
// Keep looping as long as there are packets to process this tick.
*remaining > 0
}
Ok(None) => false,
Err(e) => {
// Client is disconnected.
debug!("disconnecting client: {e:#}");
commands.entity(*entity).remove::<Client>();
false
}
}
} else {
// Client must have been deleted in the last run of the schedule.
false
}
});
state.apply(world);
world.run_schedule(EventLoopSchedule);
}
}

File diff suppressed because it is too large Load diff

View file

@ -3,13 +3,14 @@ use valence_protocol::packet::c2s::play::click_slot::ClickMode;
use valence_protocol::packet::c2s::play::ClickSlotC2s;
use super::{Inventory, InventoryWindow, PLAYER_INVENTORY_MAIN_SLOTS_COUNT};
use crate::prelude::CursorItem;
use crate::client::CursorItem;
/// Validates a click slot packet enforcing that all fields are valid.
pub(crate) fn validate_click_slot_impossible(
pub(super) fn validate_click_slot_packet(
packet: &ClickSlotC2s,
player_inventory: &Inventory,
open_inventory: Option<&Inventory>,
cursor_item: &CursorItem,
) -> anyhow::Result<()> {
ensure!(
(packet.window_id == 0) == open_inventory.is_none(),
@ -25,7 +26,7 @@ pub(crate) fn validate_click_slot_impossible(
// check all slot ids and item counts are valid
ensure!(
packet.slots.iter().all(|s| {
packet.slot_changes.iter().all(|s| {
if !(0..=max_slot).contains(&(s.idx as u16)) {
return false;
}
@ -115,19 +116,8 @@ pub(crate) fn validate_click_slot_impossible(
ClickMode::DoubleClick => ensure!(packet.button == 0, "invalid button"),
}
Ok(())
}
// Check that items aren't being duplicated, i.e. conservation of mass.
/// Validates a click slot packet, enforcing that items can't be duplicated, eg.
/// conservation of mass.
///
/// Relies on assertions made by [`validate_click_slot_impossible`].
pub(crate) fn validate_click_slot_item_duplication(
packet: &ClickSlotC2s,
player_inventory: &Inventory,
open_inventory: Option<&Inventory>,
cursor_item: &CursorItem,
) -> anyhow::Result<()> {
let window = InventoryWindow {
player_inventory,
open_inventory,
@ -137,7 +127,10 @@ pub(crate) fn validate_click_slot_item_duplication(
ClickMode::Click => {
if packet.slot_idx == -999 {
// Clicked outside the window, so the client is dropping an item
ensure!(packet.slots.is_empty(), "slot modifications must be empty");
ensure!(
packet.slot_changes.is_empty(),
"slot modifications must be empty"
);
// Clicked outside the window
let count_deltas = calculate_net_item_delta(packet, &window, cursor_item);
@ -158,15 +151,15 @@ pub(crate) fn validate_click_slot_item_duplication(
);
} else {
ensure!(
packet.slots.len() == 1,
packet.slot_changes.len() == 1,
"click must modify one slot, got {}",
packet.slots.len()
packet.slot_changes.len()
);
let old_slot = window.slot(packet.slots[0].idx as u16);
// TODO: make sure NBT is the same
// Sometimes, the client will add nbt data to an item if it's missing, like
// "Damage" to a sword
let old_slot = window.slot(packet.slot_changes[0].idx as u16);
// TODO: make sure NBT is the same.
// Sometimes, the client will add nbt data to an item if it's missing,
// like "Damage" to a sword.
let should_swap = packet.button == 0
&& match (old_slot, cursor_item.0.as_ref()) {
(Some(old_slot), Some(cursor_item)) => old_slot.item != cursor_item.item,
@ -181,7 +174,7 @@ pub(crate) fn validate_click_slot_item_duplication(
// assert that a swap occurs
ensure!(
old_slot == packet.carried_item.as_ref()
&& cursor_item.0 == packet.slots[0].item,
&& cursor_item.0 == packet.slot_changes[0].item,
"swapped items must match"
);
} else {
@ -197,9 +190,9 @@ pub(crate) fn validate_click_slot_item_duplication(
}
ClickMode::ShiftClick => {
ensure!(
(2..=3).contains(&packet.slots.len()),
(2..=3).contains(&packet.slot_changes.len()),
"shift click must modify 2 or 3 slots, got {}",
packet.slots.len()
packet.slot_changes.len()
);
let count_deltas = calculate_net_item_delta(packet, &window, cursor_item);
@ -210,7 +203,7 @@ pub(crate) fn validate_click_slot_item_duplication(
);
let Some(item_kind) = packet
.slots
.slot_changes
.iter()
.filter_map(|s| s.item.as_ref())
.next()
@ -229,7 +222,7 @@ pub(crate) fn validate_click_slot_item_duplication(
// assert all moved items are the same kind
ensure!(
packet
.slots
.slot_changes
.iter()
.filter_map(|s| s.item.as_ref())
.all(|s| s.item == item_kind),
@ -239,9 +232,9 @@ pub(crate) fn validate_click_slot_item_duplication(
ClickMode::Hotbar => {
ensure!(
packet.slots.len() == 2,
packet.slot_changes.len() == 2,
"hotbar swap must modify two slots, got {}",
packet.slots.len()
packet.slot_changes.len()
);
let count_deltas = calculate_net_item_delta(packet, &window, cursor_item);
@ -253,32 +246,32 @@ pub(crate) fn validate_click_slot_item_duplication(
// assert that a swap occurs
let old_slots = [
window.slot(packet.slots[0].idx as u16),
window.slot(packet.slots[1].idx as u16),
window.slot(packet.slot_changes[0].idx as u16),
window.slot(packet.slot_changes[1].idx as u16),
];
ensure!(
old_slots
.iter()
.any(|s| s == &packet.slots[0].item.as_ref())
.any(|s| s == &packet.slot_changes[0].item.as_ref())
&& old_slots
.iter()
.any(|s| s == &packet.slots[1].item.as_ref()),
.any(|s| s == &packet.slot_changes[1].item.as_ref()),
"swapped items must match"
);
}
ClickMode::CreativeMiddleClick => {}
ClickMode::DropKey => {
ensure!(
packet.slots.len() == 1,
packet.slot_changes.len() == 1,
"drop key must modify exactly one slot"
);
ensure!(
packet.slot_idx == packet.slots.first().map(|s| s.idx).unwrap_or(-2),
packet.slot_idx == packet.slot_changes.first().map(|s| s.idx).unwrap_or(-2),
"slot index does not match modified slot"
);
let old_slot = window.slot(packet.slot_idx as u16);
let new_slot = packet.slots[0].item.as_ref();
let new_slot = packet.slot_changes[0].item.as_ref();
let is_transmuting = match (old_slot, new_slot) {
// TODO: make sure NBT is the same
// Sometimes, the client will add nbt data to an item if it's missing, like "Damage"
@ -312,7 +305,7 @@ pub(crate) fn validate_click_slot_item_duplication(
count_deltas
);
} else {
ensure!(packet.slots.is_empty() && packet.carried_item == cursor_item.0);
ensure!(packet.slot_changes.is_empty() && packet.carried_item == cursor_item.0);
}
}
ClickMode::DoubleClick => {
@ -340,7 +333,7 @@ fn calculate_net_item_delta(
) -> i32 {
let mut net_item_delta: i32 = 0;
for slot in &packet.slots {
for slot in &packet.slot_changes {
let old_slot = window.slot(slot.idx as u16);
let new_slot = slot.item.as_ref();
@ -379,7 +372,7 @@ mod test {
slot_idx: -999,
button: 2,
mode: ClickMode::Drag,
slots: vec![
slot_changes: vec![
Slot {
idx: 4,
item: Some(ItemStack::new(ItemKind::Diamond, 21, None)),
@ -415,7 +408,7 @@ mod test {
slot_idx: -999,
button: 2,
mode: ClickMode::Click,
slots: vec![
slot_changes: vec![
Slot {
idx: 2,
item: Some(ItemStack::new(ItemKind::Diamond, 2, None)),
@ -459,19 +452,12 @@ mod test {
mode: ClickMode::Click,
state_id: VarInt(0),
slot_idx: 0,
slots: vec![Slot { idx: 0, item: None }],
slot_changes: vec![Slot { idx: 0, item: None }],
carried_item: inventory.slot(0).cloned(),
};
validate_click_slot_impossible(&packet, &player_inventory, Some(&inventory))
validate_click_slot_packet(&packet, &player_inventory, Some(&inventory), &cursor_item)
.expect("packet should be valid");
validate_click_slot_item_duplication(
&packet,
&player_inventory,
Some(&inventory),
&cursor_item,
)
.expect("packet should not fail item duplication check");
}
#[test]
@ -487,7 +473,7 @@ mod test {
mode: ClickMode::Click,
state_id: VarInt(0),
slot_idx: 0,
slots: vec![Slot {
slot_changes: vec![Slot {
idx: 0,
item: Some(ItemStack::new(ItemKind::Diamond, 20, None)),
}],
@ -499,32 +485,18 @@ mod test {
mode: ClickMode::Click,
state_id: VarInt(0),
slot_idx: 0,
slots: vec![Slot {
slot_changes: vec![Slot {
idx: 0,
item: Some(ItemStack::new(ItemKind::Diamond, 30, None)),
}],
carried_item: None,
};
validate_click_slot_impossible(&packet1, &player_inventory, Some(&inventory1))
validate_click_slot_packet(&packet1, &player_inventory, Some(&inventory1), &cursor_item)
.expect("packet should be valid");
validate_click_slot_item_duplication(
&packet1,
&player_inventory,
Some(&inventory1),
&cursor_item,
)
.expect("packet should not fail item duplication check");
validate_click_slot_impossible(&packet2, &player_inventory, Some(&inventory2))
validate_click_slot_packet(&packet2, &player_inventory, Some(&inventory2), &cursor_item)
.expect("packet should be valid");
validate_click_slot_item_duplication(
&packet2,
&player_inventory,
Some(&inventory2),
&cursor_item,
)
.expect("packet should not fail item duplication check");
}
#[test]
@ -539,22 +511,15 @@ mod test {
mode: ClickMode::Click,
state_id: VarInt(0),
slot_idx: 0,
slots: vec![Slot {
slot_changes: vec![Slot {
idx: 0,
item: Some(ItemStack::new(ItemKind::Diamond, 64, None)),
}],
carried_item: Some(ItemStack::new(ItemKind::Diamond, 20, None)),
};
validate_click_slot_impossible(&packet, &player_inventory, Some(&inventory))
validate_click_slot_packet(&packet, &player_inventory, Some(&inventory), &cursor_item)
.expect("packet should be valid");
validate_click_slot_item_duplication(
&packet,
&player_inventory,
Some(&inventory),
&cursor_item,
)
.expect("packet should not fail item duplication check");
}
#[test]
@ -569,22 +534,15 @@ mod test {
mode: ClickMode::Click,
state_id: VarInt(0),
slot_idx: 0,
slots: vec![Slot {
slot_changes: vec![Slot {
idx: 0,
item: Some(ItemStack::new(ItemKind::Diamond, 2, None)),
}],
carried_item: Some(ItemStack::new(ItemKind::IronIngot, 2, None)),
};
validate_click_slot_impossible(&packet, &player_inventory, Some(&inventory))
validate_click_slot_packet(&packet, &player_inventory, Some(&inventory), &cursor_item)
.expect("packet should be valid");
validate_click_slot_item_duplication(
&packet,
&player_inventory,
Some(&inventory),
&cursor_item,
)
.expect("packet should not fail item duplication check");
}
#[test]
@ -600,7 +558,7 @@ mod test {
mode: ClickMode::Click,
state_id: VarInt(0),
slot_idx: 0,
slots: vec![Slot {
slot_changes: vec![Slot {
idx: 0,
item: Some(ItemStack::new(ItemKind::Diamond, 22, None)),
}],
@ -612,7 +570,7 @@ mod test {
mode: ClickMode::Click,
state_id: VarInt(0),
slot_idx: 0,
slots: vec![Slot {
slot_changes: vec![Slot {
idx: 0,
item: Some(ItemStack::new(ItemKind::Diamond, 32, None)),
}],
@ -624,7 +582,7 @@ mod test {
mode: ClickMode::Click,
state_id: VarInt(0),
slot_idx: 0,
slots: vec![
slot_changes: vec![
Slot {
idx: 0,
item: Some(ItemStack::new(ItemKind::Diamond, 22, None)),
@ -637,35 +595,14 @@ mod test {
carried_item: None,
};
validate_click_slot_impossible(&packet1, &player_inventory, Some(&inventory1))
.expect("packet 1 should be valid");
validate_click_slot_item_duplication(
&packet1,
&player_inventory,
Some(&inventory1),
&cursor_item,
)
.expect_err("packet 1 should fail item duplication check");
validate_click_slot_packet(&packet1, &player_inventory, Some(&inventory1), &cursor_item)
.expect_err("packet 1 should fail item duplication check");
validate_click_slot_impossible(&packet2, &player_inventory, Some(&inventory2))
.expect("packet 2 should be valid");
validate_click_slot_item_duplication(
&packet2,
&player_inventory,
Some(&inventory2),
&cursor_item,
)
.expect_err("packet 2 should fail item duplication check");
validate_click_slot_packet(&packet2, &player_inventory, Some(&inventory2), &cursor_item)
.expect_err("packet 2 should fail item duplication check");
validate_click_slot_impossible(&packet3, &player_inventory, Some(&inventory1))
.expect("packet 3 should be valid");
validate_click_slot_item_duplication(
&packet3,
&player_inventory,
Some(&inventory1),
&cursor_item,
)
.expect_err("packet 3 should fail item duplication check");
validate_click_slot_packet(&packet3, &player_inventory, Some(&inventory1), &cursor_item)
.expect_err("packet 3 should fail item duplication check");
}
#[test]
@ -683,7 +620,7 @@ mod test {
mode: ClickMode::ShiftClick,
state_id: VarInt(0),
slot_idx: 9,
slots: vec![
slot_changes: vec![
Slot { idx: 9, item: None },
Slot {
idx: 36,
@ -698,7 +635,7 @@ mod test {
mode: ClickMode::Hotbar,
state_id: VarInt(0),
slot_idx: 9,
slots: vec![
slot_changes: vec![
Slot { idx: 9, item: None },
Slot {
idx: 36,
@ -713,7 +650,7 @@ mod test {
mode: ClickMode::Click,
state_id: VarInt(0),
slot_idx: 9,
slots: vec![Slot { idx: 9, item: None }],
slot_changes: vec![Slot { idx: 9, item: None }],
carried_item: Some(ItemStack::new(ItemKind::GoldIngot, 2, None)),
},
ClickSlotC2s {
@ -722,7 +659,7 @@ mod test {
mode: ClickMode::DropKey,
state_id: VarInt(0),
slot_idx: 9,
slots: vec![Slot {
slot_changes: vec![Slot {
idx: 9,
item: Some(ItemStack::new(ItemKind::GoldIngot, 1, None)),
}],
@ -731,12 +668,9 @@ mod test {
];
for (i, packet) in packets.iter().enumerate() {
validate_click_slot_impossible(packet, &player_inventory, None)
.unwrap_or_else(|e| panic!("packet {i} should be valid: {e}"));
validate_click_slot_item_duplication(packet, &player_inventory, None, &cursor_item)
.expect_err(&format!(
"packet {i} passed item duplication check when it should have failed"
));
validate_click_slot_packet(packet, &player_inventory, None, &cursor_item).expect_err(
&format!("packet {i} passed item duplication check when it should have failed"),
);
}
}
@ -753,7 +687,7 @@ mod test {
slot_idx: 9,
button: 0,
mode: ClickMode::ShiftClick,
slots: vec![
slot_changes: vec![
Slot {
idx: 37,
item: Some(ItemStack::new(ItemKind::Diamond, 32, None)),
@ -767,10 +701,8 @@ mod test {
carried_item: None,
};
validate_click_slot_impossible(&packet, &player_inventory, None)
validate_click_slot_packet(&packet, &player_inventory, None, &cursor_item)
.expect("packet should be valid");
validate_click_slot_item_duplication(&packet, &player_inventory, None, &cursor_item)
.expect("packet should pass item duplication check");
}
#[test]
@ -785,14 +717,12 @@ mod test {
slot_idx: 9,
button: 0,
mode: ClickMode::Click,
slots: vec![Slot { idx: 9, item: None }],
slot_changes: vec![Slot { idx: 9, item: None }],
carried_item: Some(ItemStack::new(ItemKind::Apple, 100, None)),
};
validate_click_slot_impossible(&packet, &player_inventory, None)
validate_click_slot_packet(&packet, &player_inventory, None, &cursor_item)
.expect("packet should be valid");
validate_click_slot_item_duplication(&packet, &player_inventory, None, &cursor_item)
.expect("packet should pass item duplication check");
}
#[test]
@ -806,16 +736,14 @@ mod test {
slot_idx: 9,
button: 0,
mode: ClickMode::Click,
slots: vec![Slot {
slot_changes: vec![Slot {
idx: 9,
item: Some(ItemStack::new(ItemKind::Apple, 64, None)),
}],
carried_item: Some(ItemStack::new(ItemKind::Apple, 36, None)),
};
validate_click_slot_impossible(&packet, &player_inventory, None)
validate_click_slot_packet(&packet, &player_inventory, None, &cursor_item)
.expect("packet should be valid");
validate_click_slot_item_duplication(&packet, &player_inventory, None, &cursor_item)
.expect("packet should pass item duplication check");
}
}

View file

@ -32,6 +32,7 @@ pub mod component;
pub mod config;
pub mod dimension;
pub mod entity;
pub mod event_loop;
pub mod instance;
pub mod inventory;
pub mod packet;
@ -50,14 +51,21 @@ pub mod prelude {
pub use bevy_app::prelude::*;
pub use bevy_ecs::prelude::*;
pub use biome::{Biome, BiomeId, BiomeRegistry};
pub use client::event::{EventLoopSchedule, EventLoopSet};
pub use client::*;
pub use client::action::*;
pub use client::command::*;
pub use client::interact_entity::*;
pub use client::{
despawn_disconnected_clients, Client, CompassPos, CursorItem, DeathLocation,
HasRespawnScreen, HashedSeed, Ip, IsDebug, IsFlat, IsHardcore, OldView, OldViewDistance,
OpLevel, PrevGameMode, ReducedDebugInfo, View, ViewDistance,
};
pub use component::*;
pub use config::{
AsyncCallbacks, ConnectionMode, PlayerSampleEntry, ServerListPing, ServerPlugin,
};
pub use dimension::{DimensionType, DimensionTypeRegistry};
pub use entity::{EntityAnimation, EntityKind, EntityManager, EntityStatus, HeadYaw};
pub use event_loop::{EventLoopSchedule, EventLoopSet};
pub use glam::DVec3;
pub use instance::{Block, BlockMut, BlockRef, Chunk, Instance};
pub use inventory::{

View file

@ -1,7 +1,7 @@
use std::io::Write;
use tracing::warn;
use valence_protocol::codec::{encode_packet, encode_packet_compressed, PacketEncoder};
use valence_protocol::encoder::{encode_packet, encode_packet_compressed, PacketEncoder};
use valence_protocol::Packet;
/// Types that can have packets written to them.

View file

@ -16,10 +16,9 @@ use valence_protocol::packet::s2c::play::{PlayerListHeaderS2c, PlayerRemoveS2c};
use valence_protocol::text::Text;
use valence_protocol::types::Property;
use crate::client::Client;
use crate::client::{Client, FlushPacketsSet};
use crate::component::{GameMode, Ping, Properties, UniqueId, Username};
use crate::packet::{PacketWriter, WritePacket};
use crate::prelude::FlushPacketsSet;
use crate::server::Server;
/// The global list of players on a server visible by pressing the tab key by

View file

@ -16,15 +16,14 @@ use uuid::Uuid;
use valence_protocol::types::Property;
use crate::biome::BiomePlugin;
use crate::client::event::EventLoopSet;
use crate::client::{ClientBundle, ClientPlugin};
use crate::config::{AsyncCallbacks, ConnectionMode, ServerPlugin};
use crate::dimension::DimensionPlugin;
use crate::entity::EntityPlugin;
use crate::event_loop::{EventLoopPlugin, RunEventLoopSet};
use crate::instance::InstancePlugin;
use crate::inventory::{InventoryPlugin, InventorySettings};
use crate::inventory::InventoryPlugin;
use crate::player_list::PlayerListPlugin;
use crate::prelude::event::ClientEventPlugin;
use crate::prelude::ComponentPlugin;
use crate::registry_codec::RegistryCodecPlugin;
use crate::server::connect::do_accept_loop;
@ -34,6 +33,8 @@ mod byte_channel;
mod connect;
pub(crate) mod connection;
use connection::NewClientArgs;
/// Contains global server state accessible as a [`Resource`].
#[derive(Resource)]
pub struct Server {
@ -82,9 +83,9 @@ struct SharedServerInner {
/// to store the runtime here so we don't drop it.
_tokio_runtime: Option<Runtime>,
/// Sender for new clients past the login stage.
new_clients_send: Sender<ClientBundle>,
new_clients_send: Sender<NewClientArgs>,
/// Receiver for new clients past the login stage.
new_clients_recv: Receiver<ClientBundle>,
new_clients_recv: Receiver<NewClientArgs>,
/// A semaphore used to limit the number of simultaneous connections to the
/// server. Closing this semaphore stops new connections.
connection_sema: Arc<Semaphore>,
@ -228,11 +229,11 @@ pub fn build_plugin(
// System to spawn new clients.
let spawn_new_clients = move |world: &mut World| {
for _ in 0..shared.0.new_clients_recv.len() {
let Ok(client) = shared.0.new_clients_recv.try_recv() else {
let Ok(args) = shared.0.new_clients_recv.try_recv() else {
break
};
world.spawn(client);
world.spawn(ClientBundle::new(args.info, args.conn, args.enc));
}
};
@ -240,7 +241,6 @@ pub fn build_plugin(
// Insert resources.
app.insert_resource(server);
app.insert_resource(InventorySettings::default());
// Make the app loop forever at the configured TPS.
{
@ -262,18 +262,18 @@ pub fn build_plugin(
app.add_system(
spawn_new_clients
.in_base_set(CoreSet::PreUpdate)
.before(EventLoopSet),
.before(RunEventLoopSet),
);
app.add_system(increment_tick_counter.in_base_set(CoreSet::Last));
// Add internal plugins.
app.add_plugin(RegistryCodecPlugin)
app.add_plugin(EventLoopPlugin)
.add_plugin(RegistryCodecPlugin)
.add_plugin(BiomePlugin)
.add_plugin(DimensionPlugin)
.add_plugin(ComponentPlugin)
.add_plugin(ClientPlugin)
.add_plugin(ClientEventPlugin)
.add_plugin(EntityPlugin)
.add_plugin(InstancePlugin)
.add_plugin(InventoryPlugin)

View file

@ -21,7 +21,8 @@ use tokio::net::{TcpListener, TcpStream};
use tokio::sync::OwnedSemaphorePermit;
use tracing::{error, info, instrument, trace, warn};
use uuid::Uuid;
use valence_protocol::codec::{PacketDecoder, PacketEncoder};
use valence_protocol::decoder::PacketDecoder;
use valence_protocol::encoder::PacketEncoder;
use valence_protocol::packet::c2s::handshake::handshake::NextState;
use valence_protocol::packet::c2s::handshake::HandshakeC2s;
use valence_protocol::packet::c2s::login::{LoginHelloC2s, LoginKeyC2s, LoginQueryResponseC2s};
@ -149,7 +150,7 @@ async fn handle_handshake(
.context("error handling login")?
{
Some(info) => {
let client = conn.into_client_bundle(
let client = conn.into_client_args(
info,
shared.0.incoming_capacity,
shared.0.outgoing_capacity,

View file

@ -1,21 +1,22 @@
use std::io;
use std::io::ErrorKind;
use std::time::Duration;
use std::sync::Arc;
use std::time::{Duration, Instant};
use std::{io, mem};
use anyhow::bail;
use bytes::BytesMut;
use bytes::{Buf, BytesMut};
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
use tokio::sync::OwnedSemaphorePermit;
use tokio::sync::{OwnedSemaphorePermit, Semaphore};
use tokio::task::JoinHandle;
use tokio::time::timeout;
use tracing::debug;
use valence_protocol::codec::{PacketDecoder, PacketEncoder};
use valence_protocol::Packet;
use tracing::{debug, warn};
use valence_protocol::decoder::{decode_packet, PacketDecoder};
use valence_protocol::encoder::PacketEncoder;
use valence_protocol::var_int::VarInt;
use valence_protocol::{Decode, Packet};
use crate::client::{ClientBundle, ClientConnection};
use crate::server::byte_channel::{
byte_channel, ByteReceiver, ByteSender, TryRecvError, TrySendError,
};
use crate::client::{ClientConnection, ReceivedPacket};
use crate::server::byte_channel::{byte_channel, ByteSender, TrySendError};
use crate::server::NewClientInfo;
pub(super) struct InitialConnection<R, W> {
@ -23,6 +24,7 @@ pub(super) struct InitialConnection<R, W> {
writer: W,
enc: PacketEncoder,
dec: PacketDecoder,
frame: BytesMut,
timeout: Duration,
permit: OwnedSemaphorePermit,
}
@ -47,6 +49,7 @@ where
writer,
enc,
dec,
frame: BytesMut::new(),
timeout,
permit,
}
@ -67,30 +70,11 @@ where
P: Packet<'a>,
{
timeout(self.timeout, async {
while !self.dec.has_next_packet()? {
self.dec.reserve(READ_BUF_SIZE);
let mut buf = self.dec.take_capacity();
if self.reader.read_buf(&mut buf).await? == 0 {
return Err(io::Error::from(ErrorKind::UnexpectedEof).into());
}
// This should always be an O(1) unsplit because we reserved space earlier and
// the call to `read_buf` shouldn't have grown the allocation.
self.dec.queue_bytes(buf);
}
Ok(self
.dec
.try_next_packet()?
.expect("decoder said it had another packet"))
// The following is what I want to write but can't due to borrow
// checker errors I don't understand.
/*
loop {
if let Some(pkt) = self.dec.try_next_packet()? {
return Ok(pkt);
if let Some(frame) = self.dec.try_next_packet()? {
self.frame = frame;
return decode_packet(&self.frame);
}
self.dec.reserve(READ_BUF_SIZE);
@ -104,7 +88,6 @@ where
// the call to `read_buf` shouldn't have grown the allocation.
self.dec.queue_bytes(buf);
}
*/
})
.await?
}
@ -112,7 +95,7 @@ where
#[allow(dead_code)]
pub fn set_compression(&mut self, threshold: Option<u32>) {
self.enc.set_compression(threshold);
self.dec.set_compression(threshold.is_some());
self.dec.set_compression(threshold);
}
pub fn enable_encryption(&mut self, key: &[u8; 16]) {
@ -120,34 +103,99 @@ where
self.dec.enable_encryption(key);
}
pub fn into_client_bundle(
pub fn into_client_args(
mut self,
info: NewClientInfo,
incoming_limit: usize,
outgoing_limit: usize,
) -> ClientBundle
) -> NewClientArgs
where
R: Send + 'static,
W: Send + 'static,
{
let (mut incoming_sender, incoming_receiver) = byte_channel(incoming_limit);
let (incoming_sender, incoming_receiver) = flume::unbounded();
let recv_sem = Arc::new(Semaphore::new(incoming_limit));
let recv_sem_clone = recv_sem.clone();
let reader_task = tokio::spawn(async move {
loop {
let mut buf = incoming_sender.take_capacity(READ_BUF_SIZE);
let mut buf = BytesMut::new();
match self.reader.read_buf(&mut buf).await {
Ok(0) => break,
loop {
let mut data = match self.dec.try_next_packet() {
Ok(Some(data)) => data,
Ok(None) => {
// Incomplete packet. Need more data.
buf.reserve(READ_BUF_SIZE);
match self.reader.read_buf(&mut buf).await {
Ok(0) => break, // Reader is at EOF.
Ok(_) => {}
Err(e) => {
debug!("error reading data from stream: {e}");
break;
}
}
self.dec.queue_bytes(buf.split());
continue;
}
Err(e) => {
debug!("error reading packet data: {e}");
warn!("error decoding packet frame: {e:#}");
break;
}
_ => {}
};
let timestamp = Instant::now();
// Remove the packet ID from the front of the data.
let packet_id = {
let mut r = &data[..];
match VarInt::decode(&mut r) {
Ok(id) => {
data.advance(data.len() - r.len());
id.0
}
Err(e) => {
warn!("failed to decode packet ID: {e:#}");
break;
}
}
};
// Estimate memory usage of this packet.
let cost = mem::size_of::<ReceivedPacket>() + data.len();
if cost > incoming_limit {
debug!(
cost,
incoming_limit,
"cost of received packet is greater than the incoming memory limit"
);
// We would never acquire enough permits, so we should exit instead of getting
// stuck.
break;
}
// This should always be an O(1) unsplit because we reserved space earlier.
if let Err(e) = incoming_sender.send_async(buf).await {
debug!("error sending packet data: {e}");
// Wait until there's enough space for this packet.
let Ok(permits) = recv_sem.acquire_many(cost as u32).await else {
// Semaphore closed.
break;
};
// The permits will be added back on the other side of the channel.
permits.forget();
let packet = ReceivedPacket {
timestamp,
id: packet_id,
data: data.freeze(),
};
if incoming_sender.try_send(packet).is_err() {
// Channel closed.
break;
}
}
@ -166,32 +214,41 @@ where
};
if let Err(e) = self.writer.write_all(&bytes).await {
debug!("error writing packet data: {e}");
debug!("error writing data to stream: {e}");
}
}
});
ClientBundle::new(
NewClientArgs {
info,
Box::new(RealClientConnection {
conn: Box::new(RealClientConnection {
send: outgoing_sender,
recv: incoming_receiver,
_permit: self.permit,
recv_sem: recv_sem_clone,
_client_permit: self.permit,
reader_task,
writer_task,
}),
self.enc,
self.dec,
)
enc: self.enc,
}
}
}
pub struct NewClientArgs {
pub info: NewClientInfo,
pub conn: Box<dyn ClientConnection>,
pub enc: PacketEncoder,
}
struct RealClientConnection {
send: ByteSender,
recv: ByteReceiver,
/// Ensures that we don't allow more connections to the server until the
/// client is dropped.
_permit: OwnedSemaphorePermit,
recv: flume::Receiver<ReceivedPacket>,
/// Limits the amount of data queued in the `recv` channel. Each permit
/// represents one byte.
recv_sem: Arc<Semaphore>,
/// Limits the number of new clients that can connect to the server. Permit
/// is released when the connection is dropped.
_client_permit: OwnedSemaphorePermit,
reader_task: JoinHandle<()>,
writer_task: JoinHandle<()>,
}
@ -215,11 +272,22 @@ impl ClientConnection for RealClientConnection {
}
}
fn try_recv(&mut self) -> anyhow::Result<BytesMut> {
fn try_recv(&mut self) -> anyhow::Result<Option<ReceivedPacket>> {
match self.recv.try_recv() {
Ok(bytes) => Ok(bytes),
Err(TryRecvError::Empty) => Ok(BytesMut::new()),
Err(TryRecvError::Disconnected) => bail!("client disconnected"),
Ok(packet) => {
let cost = mem::size_of::<ReceivedPacket>() + packet.data.len();
// Add the permits back that we removed eariler.
self.recv_sem.add_permits(cost);
Ok(Some(packet))
}
Err(flume::TryRecvError::Empty) => Ok(None),
Err(flume::TryRecvError::Disconnected) => bail!("client disconnected"),
}
}
fn len(&self) -> usize {
self.recv.len()
}
}

View file

@ -97,7 +97,7 @@ mod tests {
/// A unit test where we want to test what packets are sent to the client.
#[test]
fn example_test_open_inventory() -> anyhow::Result<()> {
fn example_test_open_inventory() {
let mut app = App::new();
let (client_ent, mut client_helper) = scenario_single_client(&mut app);
@ -122,7 +122,7 @@ mod tests {
app.world
.get::<Client>(client_ent)
.expect("client not found");
let sent_packets = client_helper.collect_sent()?;
let sent_packets = client_helper.collect_sent();
assert_packet_count!(sent_packets, 1, S2cPlayPacket::OpenScreenS2c(_));
assert_packet_count!(sent_packets, 1, S2cPlayPacket::InventoryS2c(_));
@ -131,7 +131,5 @@ mod tests {
S2cPlayPacket::OpenScreenS2c(_),
S2cPlayPacket::InventoryS2c(_)
);
Ok(())
}
}

View file

@ -1,14 +1,18 @@
use std::collections::VecDeque;
use std::sync::{Arc, Mutex};
use std::time::Instant;
use bevy_app::prelude::*;
use bevy_ecs::prelude::*;
use bevy_ecs::schedule::{LogLevel, ScheduleBuildSettings};
use bytes::BytesMut;
use valence_protocol::codec::{PacketDecoder, PacketEncoder};
use bytes::{Buf, BufMut, BytesMut};
use valence_protocol::decoder::{decode_packet, PacketDecoder};
use valence_protocol::encoder::PacketEncoder;
use valence_protocol::packet::S2cPlayPacket;
use valence_protocol::var_int::VarInt;
use valence_protocol::{ident, Packet};
use crate::client::{ClientBundle, ClientConnection};
use crate::client::{ClientBundle, ClientConnection, ReceivedPacket};
use crate::component::Location;
use crate::config::{ConnectionMode, ServerPlugin};
use crate::instance::Instance;
@ -21,8 +25,7 @@ use crate::server::{NewClientInfo, Server};
pub(crate) fn create_mock_client(client_info: NewClientInfo) -> (ClientBundle, MockClientHelper) {
let mock_connection = MockClientConnection::new();
let enc = PacketEncoder::new();
let dec = PacketDecoder::new();
let bundle = ClientBundle::new(client_info, Box::new(mock_connection.clone()), enc, dec);
let bundle = ClientBundle::new(client_info, Box::new(mock_connection.clone()), enc);
(bundle, MockClientHelper::new(mock_connection))
}
@ -42,13 +45,13 @@ pub fn gen_client_info(username: impl Into<String>) -> NewClientInfo {
/// Safe to clone, but note that the clone will share the same buffers.
#[derive(Clone)]
pub(crate) struct MockClientConnection {
buffers: Arc<Mutex<MockClientBuffers>>,
inner: Arc<Mutex<MockClientConnectionInner>>,
}
struct MockClientBuffers {
struct MockClientConnectionInner {
/// The queue of packets to receive from the client to be processed by the
/// server.
recv_buf: BytesMut,
recv_buf: VecDeque<ReceivedPacket>,
/// The queue of packets to send from the server to the client.
send_buf: BytesMut,
}
@ -56,63 +59,49 @@ struct MockClientBuffers {
impl MockClientConnection {
pub fn new() -> Self {
Self {
buffers: Arc::new(Mutex::new(MockClientBuffers {
recv_buf: BytesMut::new(),
inner: Arc::new(Mutex::new(MockClientConnectionInner {
recv_buf: VecDeque::new(),
send_buf: BytesMut::new(),
})),
}
}
pub fn inject_recv(&mut self, bytes: BytesMut) {
self.buffers.lock().unwrap().recv_buf.unsplit(bytes);
/// Injects a (Packet ID + data) frame to be received by the server.
pub fn inject_recv(&mut self, mut bytes: BytesMut) {
let id = VarInt::decode_partial((&mut bytes).reader()).expect("failed to decode packet ID");
self.inner
.lock()
.unwrap()
.recv_buf
.push_back(ReceivedPacket {
timestamp: Instant::now(),
id,
data: bytes.freeze(),
});
}
pub fn take_sent(&mut self) -> BytesMut {
self.buffers.lock().unwrap().send_buf.split()
self.inner.lock().unwrap().send_buf.split()
}
pub fn clear_sent(&mut self) {
self.buffers.lock().unwrap().send_buf.clear();
self.inner.lock().unwrap().send_buf.clear();
}
}
impl ClientConnection for MockClientConnection {
fn try_send(&mut self, bytes: BytesMut) -> anyhow::Result<()> {
self.buffers.lock().unwrap().send_buf.unsplit(bytes);
self.inner.lock().unwrap().send_buf.unsplit(bytes);
Ok(())
}
fn try_recv(&mut self) -> anyhow::Result<BytesMut> {
Ok(self.buffers.lock().unwrap().recv_buf.split())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_mock_client_recv() -> anyhow::Result<()> {
let msg = 0xdeadbeefu32.to_be_bytes();
let b = BytesMut::from(&msg[..]);
let mut client = MockClientConnection::new();
client.inject_recv(b);
let b = client.try_recv()?;
assert_eq!(b, BytesMut::from(&msg[..]));
Ok(())
fn try_recv(&mut self) -> anyhow::Result<Option<ReceivedPacket>> {
Ok(self.inner.lock().unwrap().recv_buf.pop_front())
}
#[test]
fn test_mock_client_send() -> anyhow::Result<()> {
let msg = 0xdeadbeefu32.to_be_bytes();
let b = BytesMut::from(&msg[..]);
let mut client = MockClientConnection::new();
client.try_send(b)?;
let b = client.take_sent();
assert_eq!(b, BytesMut::from(&msg[..]));
Ok(())
fn len(&self) -> usize {
self.inner.lock().unwrap().recv_buf.len()
}
}
@ -120,33 +109,49 @@ mod tests {
/// and read packets from the send stream.
pub struct MockClientHelper {
conn: MockClientConnection,
enc: PacketEncoder,
dec: PacketDecoder,
scratch: BytesMut,
collected_frames: Vec<BytesMut>,
}
impl MockClientHelper {
fn new(conn: MockClientConnection) -> Self {
Self {
conn,
enc: PacketEncoder::new(),
dec: PacketDecoder::new(),
scratch: BytesMut::new(),
collected_frames: vec![],
}
}
/// Inject a packet to be treated as a packet inbound to the server. Panics
/// if the packet cannot be sent.
pub fn send<'a>(&mut self, packet: &impl Packet<'a>) {
self.enc
.append_packet(packet)
packet
.encode_packet((&mut self.scratch).writer())
.expect("failed to encode packet");
self.conn.inject_recv(self.enc.take());
self.conn.inject_recv(self.scratch.split());
}
/// Collect all packets that have been sent to the client.
pub fn collect_sent<'a>(&'a mut self) -> anyhow::Result<Vec<S2cPlayPacket<'a>>> {
pub fn collect_sent(&mut self) -> Vec<S2cPlayPacket> {
self.dec.queue_bytes(self.conn.take_sent());
self.dec.collect_into_vec::<S2cPlayPacket<'a>>()
self.collected_frames.clear();
while let Some(frame) = self
.dec
.try_next_packet()
.expect("failed to decode packet frame")
{
self.collected_frames.push(frame);
}
self.collected_frames
.iter()
.map(|frame| decode_packet(frame).expect("failed to decode packet"))
.collect()
}
pub fn clear_sent(&mut self) {

View file

@ -21,6 +21,7 @@ use bevy_ecs::prelude::*;
use valence_protocol::packet::s2c::play::game_state_change::GameEventKind;
use valence_protocol::packet::s2c::play::GameStateChangeS2c;
use crate::client::FlushPacketsSet;
use crate::instance::WriteUpdatePacketsToInstancesSet;
use crate::packet::WritePacket;
use crate::prelude::*;
@ -257,7 +258,6 @@ impl Plugin for WeatherPlugin {
#[cfg(test)]
mod test {
use anyhow::Ok;
use bevy_app::App;
use valence_protocol::packet::S2cPlayPacket;
@ -306,7 +306,7 @@ mod test {
}
#[test]
fn test_weather_instance() -> anyhow::Result<()> {
fn test_weather_instance() {
let mut app = App::new();
let (_, mut client_helper) = scenario_single_client(&mut app);
@ -353,15 +353,13 @@ mod test {
}
// Make assertions.
let sent_packets = client_helper.collect_sent()?;
let sent_packets = client_helper.collect_sent();
assert_weather_packets(sent_packets);
Ok(())
}
#[test]
fn test_weather_client() -> anyhow::Result<()> {
fn test_weather_client() {
let mut app = App::new();
let (_, mut client_helper) = scenario_single_client(&mut app);
@ -408,10 +406,8 @@ mod test {
}
// Make assertions.
let sent_packets = client_helper.collect_sent()?;
let sent_packets = client_helper.collect_sent();
assert_weather_packets(sent_packets);
Ok(())
}
}

View file

@ -6,8 +6,6 @@ use std::thread;
use clap::Parser;
use flume::{Receiver, Sender};
use tracing::warn;
use valence::client::{default_event_handler, despawn_disconnected_clients};
use valence::entity::player::PlayerEntityBundle;
use valence::prelude::*;
use valence_anvil::{AnvilChunk, AnvilWorld};
@ -67,7 +65,6 @@ pub fn main() {
.add_plugin(ServerPlugin::new(()))
.insert_resource(game_state)
.add_startup_system(setup)
.add_system(default_event_handler.in_schedule(EventLoopSchedule))
.add_systems(
(
init_clients,
@ -92,20 +89,14 @@ fn setup(
}
fn init_clients(
mut clients: Query<(Entity, &mut GameMode, &mut IsFlat, &UniqueId), Added<Client>>,
mut clients: Query<(&mut Location, &mut Position, &mut GameMode, &mut IsFlat), Added<Client>>,
instances: Query<Entity, With<Instance>>,
mut commands: Commands,
) {
for (entity, mut game_mode, mut is_flat, uuid) in &mut clients {
for (mut loc, mut pos, mut game_mode, mut is_flat) in &mut clients {
loc.0 = instances.single();
pos.set(SPAWN_POS);
*game_mode = GameMode::Creative;
is_flat.0 = true;
commands.entity(entity).insert(PlayerEntityBundle {
location: Location(instances.single()),
position: Position(SPAWN_POS),
uuid: *uuid,
..Default::default()
});
}
}

View file

@ -7,9 +7,8 @@ use valence_nbt::{compound, List};
use valence_protocol::array::LengthPrefixedArray;
use valence_protocol::block::{BlockKind, BlockState, PropName, PropValue};
use valence_protocol::byte_angle::ByteAngle;
use valence_protocol::codec::{
encode_packet, encode_packet_compressed, PacketDecoder, PacketEncoder,
};
use valence_protocol::decoder::{decode_packet, PacketDecoder};
use valence_protocol::encoder::{encode_packet, encode_packet_compressed, PacketEncoder};
use valence_protocol::item::ItemKind;
use valence_protocol::packet::s2c::play::{ChunkDataS2c, EntitySpawnS2c, PlayerListHeaderS2c};
use valence_protocol::text::{Color, TextFormat};
@ -233,7 +232,7 @@ fn packets(c: &mut Criterion) {
let decoder = black_box(&mut decoder);
decoder.queue_slice(&packet_buf);
decoder.try_next_packet::<ChunkDataS2c>().unwrap();
decode_packet::<ChunkDataS2c>(&decoder.try_next_packet().unwrap().unwrap()).unwrap();
black_box(decoder);
});
@ -247,7 +246,8 @@ fn packets(c: &mut Criterion) {
let decoder = black_box(&mut decoder);
decoder.queue_slice(&packet_buf);
decoder.try_next_packet::<PlayerListHeaderS2c>().unwrap();
decode_packet::<PlayerListHeaderS2c>(&decoder.try_next_packet().unwrap().unwrap())
.unwrap();
black_box(decoder);
});
@ -261,13 +261,13 @@ fn packets(c: &mut Criterion) {
let decoder = black_box(&mut decoder);
decoder.queue_slice(&packet_buf);
decoder.try_next_packet::<EntitySpawnS2c>().unwrap();
decode_packet::<EntitySpawnS2c>(&decoder.try_next_packet().unwrap().unwrap()).unwrap();
black_box(decoder);
});
});
decoder.set_compression(true);
decoder.set_compression(Some(256));
let mut scratch = vec![];
@ -279,7 +279,7 @@ fn packets(c: &mut Criterion) {
let decoder = black_box(&mut decoder);
decoder.queue_slice(&packet_buf);
decoder.try_next_packet::<ChunkDataS2c>().unwrap();
decode_packet::<ChunkDataS2c>(&decoder.try_next_packet().unwrap().unwrap()).unwrap();
black_box(decoder);
});
@ -299,7 +299,8 @@ fn packets(c: &mut Criterion) {
let decoder = black_box(&mut decoder);
decoder.queue_slice(&packet_buf);
decoder.try_next_packet::<PlayerListHeaderS2c>().unwrap();
decode_packet::<PlayerListHeaderS2c>(&decoder.try_next_packet().unwrap().unwrap())
.unwrap();
black_box(decoder);
});
@ -313,7 +314,7 @@ fn packets(c: &mut Criterion) {
let decoder = black_box(&mut decoder);
decoder.queue_slice(&packet_buf);
decoder.try_next_packet::<EntitySpawnS2c>().unwrap();
decode_packet::<EntitySpawnS2c>(&decoder.try_next_packet().unwrap().unwrap()).unwrap();
black_box(decoder);
});

View file

@ -1,625 +0,0 @@
#[cfg(feature = "encryption")]
use aes::cipher::{AsyncStreamCipher, NewCipher};
use anyhow::{bail, ensure};
use bytes::{Buf, BufMut, BytesMut};
use tracing::debug;
use crate::var_int::{VarInt, VarIntDecodeError};
use crate::{Encode, Packet, Result, MAX_PACKET_SIZE};
/// The AES block cipher with a 128 bit key, using the CFB-8 mode of
/// operation.
#[cfg(feature = "encryption")]
type Cipher = cfb8::Cfb8<aes::Aes128>;
#[derive(Default)]
pub struct PacketEncoder {
buf: BytesMut,
#[cfg(feature = "compression")]
compress_buf: Vec<u8>,
#[cfg(feature = "compression")]
compression_threshold: Option<u32>,
#[cfg(feature = "encryption")]
cipher: Option<Cipher>,
}
impl PacketEncoder {
pub fn new() -> Self {
Self::default()
}
#[inline]
pub fn append_bytes(&mut self, bytes: &[u8]) {
self.buf.extend_from_slice(bytes)
}
pub fn prepend_packet<'a, P>(&mut self, pkt: &P) -> Result<()>
where
P: Packet<'a>,
{
let start_len = self.buf.len();
self.append_packet(pkt)?;
let end_len = self.buf.len();
let total_packet_len = end_len - start_len;
// 1) Move everything back by the length of the packet.
// 2) Move the packet to the new space at the front.
// 3) Truncate the old packet away.
self.buf.put_bytes(0, total_packet_len);
self.buf.copy_within(..end_len, total_packet_len);
self.buf.copy_within(total_packet_len + start_len.., 0);
self.buf.truncate(end_len);
Ok(())
}
pub fn append_packet<'a, P>(&mut self, pkt: &P) -> Result<()>
where
P: Packet<'a>,
{
let start_len = self.buf.len();
pkt.encode_packet((&mut self.buf).writer())?;
let data_len = self.buf.len() - start_len;
#[cfg(feature = "compression")]
if let Some(threshold) = self.compression_threshold {
use std::io::Read;
use flate2::bufread::ZlibEncoder;
use flate2::Compression;
if data_len > threshold as usize {
let mut z = ZlibEncoder::new(&self.buf[start_len..], Compression::new(4));
self.compress_buf.clear();
let data_len_size = VarInt(data_len as i32).written_size();
let packet_len = data_len_size + z.read_to_end(&mut self.compress_buf)?;
ensure!(
packet_len <= MAX_PACKET_SIZE as usize,
"packet exceeds maximum length"
);
drop(z);
self.buf.truncate(start_len);
let mut writer = (&mut self.buf).writer();
VarInt(packet_len as i32).encode(&mut writer)?;
VarInt(data_len as i32).encode(&mut writer)?;
self.buf.extend_from_slice(&self.compress_buf);
} else {
let data_len_size = 1;
let packet_len = data_len_size + data_len;
ensure!(
packet_len <= MAX_PACKET_SIZE as usize,
"packet exceeds maximum length"
);
let packet_len_size = VarInt(packet_len as i32).written_size();
let data_prefix_len = packet_len_size + data_len_size;
self.buf.put_bytes(0, data_prefix_len);
self.buf
.copy_within(start_len..start_len + data_len, start_len + data_prefix_len);
let mut front = &mut self.buf[start_len..];
VarInt(packet_len as i32).encode(&mut front)?;
// Zero for no compression on this packet.
VarInt(0).encode(front)?;
}
return Ok(());
}
let packet_len = data_len;
ensure!(
packet_len <= MAX_PACKET_SIZE as usize,
"packet exceeds maximum length"
);
let packet_len_size = VarInt(packet_len as i32).written_size();
self.buf.put_bytes(0, packet_len_size);
self.buf
.copy_within(start_len..start_len + data_len, start_len + packet_len_size);
let front = &mut self.buf[start_len..];
VarInt(packet_len as i32).encode(front)?;
Ok(())
}
/// Takes all the packets written so far and encrypts them if encryption is
/// enabled.
pub fn take(&mut self) -> BytesMut {
#[cfg(feature = "encryption")]
if let Some(cipher) = &mut self.cipher {
cipher.encrypt(&mut self.buf);
}
self.buf.split()
}
pub fn clear(&mut self) {
self.buf.clear();
}
#[cfg(feature = "compression")]
pub fn set_compression(&mut self, threshold: Option<u32>) {
self.compression_threshold = threshold;
}
/// Encrypts all future packets **and any packets that have
/// not been [taken] yet.**
///
/// [taken]: Self::take
#[cfg(feature = "encryption")]
pub fn enable_encryption(&mut self, key: &[u8; 16]) {
assert!(self.cipher.is_none(), "encryption is already enabled");
self.cipher = Some(NewCipher::new(key.into(), key.into()));
}
}
pub fn encode_packet<'a, P>(buf: &mut Vec<u8>, pkt: &P) -> Result<()>
where
P: Packet<'a>,
{
let start_len = buf.len();
pkt.encode_packet(&mut *buf)?;
let packet_len = buf.len() - start_len;
ensure!(
packet_len <= MAX_PACKET_SIZE as usize,
"packet exceeds maximum length"
);
let packet_len_size = VarInt(packet_len as i32).written_size();
buf.put_bytes(0, packet_len_size);
buf.copy_within(
start_len..start_len + packet_len,
start_len + packet_len_size,
);
let front = &mut buf[start_len..];
VarInt(packet_len as i32).encode(front)?;
Ok(())
}
#[cfg(feature = "compression")]
pub fn encode_packet_compressed<'a, P>(
buf: &mut Vec<u8>,
pkt: &P,
threshold: u32,
scratch: &mut Vec<u8>,
) -> Result<()>
where
P: Packet<'a>,
{
use std::io::Read;
use flate2::bufread::ZlibEncoder;
use flate2::Compression;
let start_len = buf.len();
pkt.encode_packet(&mut *buf)?;
let data_len = buf.len() - start_len;
if data_len > threshold as usize {
let mut z = ZlibEncoder::new(&buf[start_len..], Compression::new(4));
scratch.clear();
let data_len_size = VarInt(data_len as i32).written_size();
let packet_len = data_len_size + z.read_to_end(scratch)?;
ensure!(
packet_len <= MAX_PACKET_SIZE as usize,
"packet exceeds maximum length"
);
drop(z);
buf.truncate(start_len);
VarInt(packet_len as i32).encode(&mut *buf)?;
VarInt(data_len as i32).encode(&mut *buf)?;
buf.extend_from_slice(scratch);
} else {
let data_len_size = 1;
let packet_len = data_len_size + data_len;
ensure!(
packet_len <= MAX_PACKET_SIZE as usize,
"packet exceeds maximum length"
);
let packet_len_size = VarInt(packet_len as i32).written_size();
let data_prefix_len = packet_len_size + data_len_size;
buf.put_bytes(0, data_prefix_len);
buf.copy_within(start_len..start_len + data_len, start_len + data_prefix_len);
let mut front = &mut buf[start_len..];
VarInt(packet_len as i32).encode(&mut front)?;
// Zero for no compression on this packet.
VarInt(0).encode(front)?;
}
Ok(())
}
#[derive(Default)]
pub struct PacketDecoder {
buf: BytesMut,
cursor: usize,
#[cfg(feature = "compression")]
decompress_buf: Vec<u8>,
#[cfg(feature = "compression")]
compression_enabled: bool,
#[cfg(feature = "encryption")]
cipher: Option<Cipher>,
}
impl PacketDecoder {
pub fn new() -> Self {
Self::default()
}
pub fn try_next_packet<'a, P>(&'a mut self) -> Result<Option<P>>
where
P: Packet<'a>,
{
self.buf.advance(self.cursor);
self.cursor = 0;
let mut r = &self.buf[..];
let packet_len = match VarInt::decode_partial(&mut r) {
Ok(len) => len,
Err(VarIntDecodeError::Incomplete) => return Ok(None),
Err(VarIntDecodeError::TooLarge) => bail!("malformed packet length VarInt"),
};
ensure!(
(0..=MAX_PACKET_SIZE).contains(&packet_len),
"packet length of {packet_len} is out of bounds"
);
if r.len() < packet_len as usize {
return Ok(None);
}
r = &r[..packet_len as usize];
#[cfg(feature = "compression")]
let packet = if self.compression_enabled {
use std::io::Read;
use anyhow::Context;
use flate2::bufread::ZlibDecoder;
use crate::Decode;
let data_len = VarInt::decode(&mut r)?.0;
ensure!(
(0..MAX_PACKET_SIZE).contains(&data_len),
"decompressed packet length of {data_len} is out of bounds"
);
if data_len != 0 {
self.decompress_buf.clear();
self.decompress_buf.reserve_exact(data_len as usize);
let mut z = ZlibDecoder::new(r).take(data_len as u64);
z.read_to_end(&mut self.decompress_buf)
.context("decompressing packet")?;
r = &self.decompress_buf;
P::decode_packet(&mut r)?
} else {
P::decode_packet(&mut r)?
}
} else {
P::decode_packet(&mut r)?
};
#[cfg(not(feature = "compression"))]
let packet = P::decode_packet(&mut r)?;
if !r.is_empty() {
let remaining = r.len();
debug!("packet after partial decode ({remaining} bytes remain): {packet:?}");
bail!("packet contents were not read completely ({remaining} bytes remain)");
}
let total_packet_len = VarInt(packet_len).written_size() + packet_len as usize;
self.cursor = total_packet_len;
Ok(Some(packet))
}
/// Repeatedly decodes a packet type until all packets in the decoder are
/// consumed or an error occurs. The decoded packets are returned in a vec.
///
/// Intended for testing purposes with encryption and compression disabled.
#[track_caller]
pub fn collect_into_vec<'a, P>(&'a mut self) -> Result<Vec<P>>
where
P: Packet<'a>,
{
#[cfg(feature = "encryption")]
assert!(
self.cipher.is_none(),
"encryption must be disabled to use this method"
);
#[cfg(feature = "compression")]
assert!(
!self.compression_enabled,
"compression must be disabled to use this method"
);
self.buf.advance(self.cursor);
self.cursor = 0;
let mut res = vec![];
loop {
let mut r = &self.buf[self.cursor..];
let packet_len = match VarInt::decode_partial(&mut r) {
Ok(len) => len,
Err(VarIntDecodeError::Incomplete) => return Ok(res),
Err(VarIntDecodeError::TooLarge) => bail!("malformed packet length VarInt"),
};
ensure!(
(0..=MAX_PACKET_SIZE).contains(&packet_len),
"packet length of {packet_len} is out of bounds"
);
if r.len() < packet_len as usize {
return Ok(res);
}
r = &r[..packet_len as usize];
let packet = P::decode_packet(&mut r)?;
if !r.is_empty() {
let remaining = r.len();
debug!("packet after partial decode ({remaining} bytes remain): {packet:?}");
bail!("packet contents were not read completely ({remaining} bytes remain)");
}
let total_packet_len = VarInt(packet_len).written_size() + packet_len as usize;
self.cursor += total_packet_len;
res.push(packet);
}
}
pub fn has_next_packet(&self) -> Result<bool> {
let mut r = &self.buf[self.cursor..];
match VarInt::decode_partial(&mut r) {
Ok(packet_len) => {
ensure!(
(0..=MAX_PACKET_SIZE).contains(&packet_len),
"packet length of {packet_len} is out of bounds"
);
Ok(r.len() >= packet_len as usize)
}
Err(VarIntDecodeError::Incomplete) => Ok(false),
Err(VarIntDecodeError::TooLarge) => bail!("malformed packet length VarInt"),
}
}
#[cfg(feature = "compression")]
pub fn compression(&self) -> bool {
self.compression_enabled
}
#[cfg(feature = "compression")]
pub fn set_compression(&mut self, enabled: bool) {
self.compression_enabled = enabled;
}
#[cfg(feature = "encryption")]
pub fn enable_encryption(&mut self, key: &[u8; 16]) {
assert!(self.cipher.is_none(), "encryption is already enabled");
let mut cipher = Cipher::new(key.into(), key.into());
// Don't forget to decrypt the data we already have.
cipher.decrypt(&mut self.buf[self.cursor..]);
self.cipher = Some(cipher);
}
pub fn queue_bytes(&mut self, mut bytes: BytesMut) {
#![allow(unused_mut)]
#[cfg(feature = "encryption")]
if let Some(cipher) = &mut self.cipher {
cipher.decrypt(&mut bytes);
}
self.buf.unsplit(bytes);
}
pub fn queue_slice(&mut self, bytes: &[u8]) {
#[cfg(feature = "encryption")]
let len = self.buf.len();
self.buf.extend_from_slice(bytes);
#[cfg(feature = "encryption")]
if let Some(cipher) = &mut self.cipher {
cipher.decrypt(&mut self.buf[len..]);
}
}
pub fn queued_bytes(&self) -> &[u8] {
self.buf.as_ref()
}
pub fn take_capacity(&mut self) -> BytesMut {
self.buf.split_off(self.buf.len())
}
pub fn reserve(&mut self, additional: usize) {
self.buf.reserve(additional);
}
}
#[cfg(test)]
mod tests {
use std::borrow::Cow;
use super::*;
use crate::block_pos::BlockPos;
use crate::ident::Ident;
use crate::item::{ItemKind, ItemStack};
use crate::text::{Text, TextFormat};
use crate::types::Hand;
use crate::var_long::VarLong;
use crate::Decode;
#[cfg(feature = "encryption")]
const CRYPT_KEY: [u8; 16] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16];
#[derive(PartialEq, Debug, Encode, Decode, Packet)]
#[packet_id = 42]
struct TestPacket<'a> {
a: bool,
b: u8,
c: i32,
d: f32,
e: f64,
f: BlockPos,
g: Hand,
h: Ident<Cow<'a, str>>,
i: Option<ItemStack>,
j: Text,
k: VarInt,
l: VarLong,
m: &'a str,
n: &'a [u8; 10],
o: [u128; 3],
}
impl<'a> TestPacket<'a> {
fn new(n: &'a str) -> Self {
Self {
a: true,
b: 12,
c: -999,
d: 5.001,
e: 1e10,
f: BlockPos::new(1, 2, 3),
g: Hand::Off,
h: Ident::new("minecraft:whatever").unwrap(),
i: Some(ItemStack::new(ItemKind::WoodenSword, 12, None)),
j: "my ".into_text() + "fancy".italic() + " text",
k: VarInt(123),
l: VarLong(456),
m: n,
n: &[7; 10],
o: [123456789; 3],
}
}
fn check(&self, n: &'a str) {
assert_eq!(self, &Self::new(n));
}
}
#[test]
fn packets_round_trip() {
let mut buf = BytesMut::new();
let mut enc = PacketEncoder::new();
enc.append_packet(&TestPacket::new("first")).unwrap();
#[cfg(feature = "compression")]
enc.set_compression(Some(0));
enc.append_packet(&TestPacket::new("second")).unwrap();
buf.unsplit(enc.take());
#[cfg(feature = "encryption")]
enc.enable_encryption(&CRYPT_KEY);
enc.append_packet(&TestPacket::new("third")).unwrap();
enc.prepend_packet(&TestPacket::new("fourth")).unwrap();
buf.unsplit(enc.take());
let mut dec = PacketDecoder::new();
dec.queue_bytes(buf);
dec.try_next_packet::<TestPacket>()
.unwrap()
.unwrap()
.check("first");
#[cfg(feature = "compression")]
dec.set_compression(true);
dec.try_next_packet::<TestPacket>()
.unwrap()
.unwrap()
.check("second");
#[cfg(feature = "encryption")]
dec.enable_encryption(&CRYPT_KEY);
dec.try_next_packet::<TestPacket>()
.unwrap()
.unwrap()
.check("fourth");
dec.try_next_packet::<TestPacket>()
.unwrap()
.unwrap()
.check("third");
}
#[test]
fn collect_packets_into_vec() {
let packets = vec![
TestPacket::new("foo"),
TestPacket::new("bar"),
TestPacket::new("baz"),
];
let mut enc = PacketEncoder::new();
let mut dec = PacketDecoder::new();
for pkt in &packets {
enc.append_packet(pkt).unwrap();
}
dec.queue_bytes(enc.take());
let res = dec.collect_into_vec::<TestPacket>().unwrap();
assert_eq!(packets, res);
}
}

View file

@ -0,0 +1,183 @@
#[cfg(feature = "encryption")]
use aes::cipher::{AsyncStreamCipher, NewCipher};
use anyhow::{bail, ensure};
use bytes::{Buf, BufMut, BytesMut};
use crate::var_int::{VarInt, VarIntDecodeError};
use crate::{Packet, Result, MAX_PACKET_SIZE};
/// The AES block cipher with a 128 bit key, using the CFB-8 mode of
/// operation.
#[cfg(feature = "encryption")]
type Cipher = cfb8::Cfb8<aes::Aes128>;
#[derive(Default)]
pub struct PacketDecoder {
buf: BytesMut,
#[cfg(feature = "compression")]
decompress_buf: BytesMut,
#[cfg(feature = "compression")]
compression_threshold: Option<u32>,
#[cfg(feature = "encryption")]
cipher: Option<Cipher>,
}
impl PacketDecoder {
pub fn new() -> Self {
Self::default()
}
pub fn try_next_packet(&mut self) -> Result<Option<BytesMut>> {
let mut r = &self.buf[..];
let packet_len = match VarInt::decode_partial(&mut r) {
Ok(len) => len,
Err(VarIntDecodeError::Incomplete) => return Ok(None),
Err(VarIntDecodeError::TooLarge) => bail!("malformed packet length VarInt"),
};
ensure!(
(0..=MAX_PACKET_SIZE).contains(&packet_len),
"packet length of {packet_len} is out of bounds"
);
if r.len() < packet_len as usize {
// Not enough data arrived yet.
return Ok(None);
}
let packet_len_len = VarInt(packet_len).written_size();
#[cfg(feature = "compression")]
if let Some(threshold) = self.compression_threshold {
use std::io::Write;
use flate2::write::ZlibDecoder;
use crate::Decode;
r = &r[..packet_len as usize];
let data_len = VarInt::decode(&mut r)?.0;
ensure!(
(0..MAX_PACKET_SIZE).contains(&data_len),
"decompressed packet length of {data_len} is out of bounds"
);
// Is this packet compressed?
if data_len > 0 {
ensure!(
data_len as u32 > threshold,
"decompressed packet length of {data_len} is <= the compression threshold of \
{threshold}"
);
debug_assert!(self.decompress_buf.is_empty());
self.decompress_buf.put_bytes(0, data_len as usize);
// TODO: use libdeflater or zune-inflate?
let mut z = ZlibDecoder::new(&mut self.decompress_buf[..]);
z.write_all(r)?;
ensure!(
z.finish()?.is_empty(),
"decompressed packet length is shorter than expected"
);
let total_packet_len = VarInt(packet_len).written_size() + packet_len as usize;
self.buf.advance(total_packet_len);
return Ok(Some(self.decompress_buf.split()));
} else {
debug_assert_eq!(data_len, 0);
ensure!(
r.len() <= threshold as usize,
"uncompressed packet length of {} exceeds compression threshold of {}",
r.len(),
threshold
);
let remaining_len = r.len();
self.buf.advance(packet_len_len + 1);
return Ok(Some(self.buf.split_to(remaining_len)));
}
}
self.buf.advance(packet_len_len);
Ok(Some(self.buf.split_to(packet_len as usize)))
}
#[cfg(feature = "compression")]
pub fn compression(&self) -> Option<u32> {
self.compression_threshold
}
#[cfg(feature = "compression")]
pub fn set_compression(&mut self, threshold: Option<u32>) {
self.compression_threshold = threshold;
}
#[cfg(feature = "encryption")]
pub fn enable_encryption(&mut self, key: &[u8; 16]) {
assert!(self.cipher.is_none(), "encryption is already enabled");
let mut cipher = Cipher::new(key.into(), key.into());
// Don't forget to decrypt the data we already have.
cipher.decrypt(&mut self.buf);
self.cipher = Some(cipher);
}
pub fn queue_bytes(&mut self, mut bytes: BytesMut) {
#![allow(unused_mut)]
#[cfg(feature = "encryption")]
if let Some(cipher) = &mut self.cipher {
cipher.decrypt(&mut bytes);
}
self.buf.unsplit(bytes);
}
pub fn queue_slice(&mut self, bytes: &[u8]) {
#[cfg(feature = "encryption")]
let len = self.buf.len();
self.buf.extend_from_slice(bytes);
#[cfg(feature = "encryption")]
if let Some(cipher) = &mut self.cipher {
cipher.decrypt(&mut self.buf[len..]);
}
}
pub fn take_capacity(&mut self) -> BytesMut {
self.buf.split_off(self.buf.len())
}
pub fn reserve(&mut self, additional: usize) {
self.buf.reserve(additional);
}
}
/// Decodes a (packet ID + data) packet frame. An error is returned if the input
/// is not read to the end.
pub fn decode_packet<'a, P: Packet<'a>>(mut bytes: &'a [u8]) -> anyhow::Result<P> {
let pkt = P::decode_packet(&mut bytes)?;
ensure!(
bytes.is_empty(),
"missed {} bytes while decoding {}",
bytes.len(),
pkt.packet_name()
);
Ok(pkt)
}

View file

@ -0,0 +1,270 @@
use anyhow::ensure;
use bytes::{BufMut, BytesMut};
use crate::var_int::VarInt;
use crate::{Encode, Packet, MAX_PACKET_SIZE};
/// The AES block cipher with a 128 bit key, using the CFB-8 mode of
/// operation.
#[cfg(feature = "encryption")]
type Cipher = cfb8::Cfb8<aes::Aes128>;
#[derive(Default)]
pub struct PacketEncoder {
buf: BytesMut,
#[cfg(feature = "compression")]
compress_buf: Vec<u8>,
#[cfg(feature = "compression")]
compression_threshold: Option<u32>,
#[cfg(feature = "encryption")]
cipher: Option<Cipher>,
}
impl PacketEncoder {
pub fn new() -> Self {
Self::default()
}
#[inline]
pub fn append_bytes(&mut self, bytes: &[u8]) {
self.buf.extend_from_slice(bytes)
}
pub fn prepend_packet<'a, P>(&mut self, pkt: &P) -> anyhow::Result<()>
where
P: Packet<'a>,
{
let start_len = self.buf.len();
self.append_packet(pkt)?;
let end_len = self.buf.len();
let total_packet_len = end_len - start_len;
// 1) Move everything back by the length of the packet.
// 2) Move the packet to the new space at the front.
// 3) Truncate the old packet away.
self.buf.put_bytes(0, total_packet_len);
self.buf.copy_within(..end_len, total_packet_len);
self.buf.copy_within(total_packet_len + start_len.., 0);
self.buf.truncate(end_len);
Ok(())
}
pub fn append_packet<'a, P>(&mut self, pkt: &P) -> anyhow::Result<()>
where
P: Packet<'a>,
{
let start_len = self.buf.len();
pkt.encode_packet((&mut self.buf).writer())?;
let data_len = self.buf.len() - start_len;
#[cfg(feature = "compression")]
if let Some(threshold) = self.compression_threshold {
use std::io::Read;
use flate2::bufread::ZlibEncoder;
use flate2::Compression;
if data_len > threshold as usize {
let mut z = ZlibEncoder::new(&self.buf[start_len..], Compression::new(4));
self.compress_buf.clear();
let data_len_size = VarInt(data_len as i32).written_size();
let packet_len = data_len_size + z.read_to_end(&mut self.compress_buf)?;
ensure!(
packet_len <= MAX_PACKET_SIZE as usize,
"packet exceeds maximum length"
);
drop(z);
self.buf.truncate(start_len);
let mut writer = (&mut self.buf).writer();
VarInt(packet_len as i32).encode(&mut writer)?;
VarInt(data_len as i32).encode(&mut writer)?;
self.buf.extend_from_slice(&self.compress_buf);
} else {
let data_len_size = 1;
let packet_len = data_len_size + data_len;
ensure!(
packet_len <= MAX_PACKET_SIZE as usize,
"packet exceeds maximum length"
);
let packet_len_size = VarInt(packet_len as i32).written_size();
let data_prefix_len = packet_len_size + data_len_size;
self.buf.put_bytes(0, data_prefix_len);
self.buf
.copy_within(start_len..start_len + data_len, start_len + data_prefix_len);
let mut front = &mut self.buf[start_len..];
VarInt(packet_len as i32).encode(&mut front)?;
// Zero for no compression on this packet.
VarInt(0).encode(front)?;
}
return Ok(());
}
let packet_len = data_len;
ensure!(
packet_len <= MAX_PACKET_SIZE as usize,
"packet exceeds maximum length"
);
let packet_len_size = VarInt(packet_len as i32).written_size();
self.buf.put_bytes(0, packet_len_size);
self.buf
.copy_within(start_len..start_len + data_len, start_len + packet_len_size);
let front = &mut self.buf[start_len..];
VarInt(packet_len as i32).encode(front)?;
Ok(())
}
/// Takes all the packets written so far and encrypts them if encryption is
/// enabled.
pub fn take(&mut self) -> BytesMut {
#[cfg(feature = "encryption")]
if let Some(cipher) = &mut self.cipher {
use aes::cipher::AsyncStreamCipher;
cipher.encrypt(&mut self.buf);
}
self.buf.split()
}
pub fn clear(&mut self) {
self.buf.clear();
}
#[cfg(feature = "compression")]
pub fn set_compression(&mut self, threshold: Option<u32>) {
self.compression_threshold = threshold;
}
/// Encrypts all future packets **and any packets that have
/// not been [taken] yet.**
///
/// [taken]: Self::take
#[cfg(feature = "encryption")]
pub fn enable_encryption(&mut self, key: &[u8; 16]) {
use aes::cipher::NewCipher;
assert!(self.cipher.is_none(), "encryption is already enabled");
self.cipher = Some(NewCipher::new(key.into(), key.into()));
}
}
pub fn encode_packet<'a, P>(buf: &mut Vec<u8>, pkt: &P) -> anyhow::Result<()>
where
P: Packet<'a>,
{
let start_len = buf.len();
pkt.encode_packet(&mut *buf)?;
let packet_len = buf.len() - start_len;
ensure!(
packet_len <= MAX_PACKET_SIZE as usize,
"packet exceeds maximum length"
);
let packet_len_size = VarInt(packet_len as i32).written_size();
buf.put_bytes(0, packet_len_size);
buf.copy_within(
start_len..start_len + packet_len,
start_len + packet_len_size,
);
let front = &mut buf[start_len..];
VarInt(packet_len as i32).encode(front)?;
Ok(())
}
#[cfg(feature = "compression")]
pub fn encode_packet_compressed<'a, P>(
buf: &mut Vec<u8>,
pkt: &P,
threshold: u32,
scratch: &mut Vec<u8>,
) -> anyhow::Result<()>
where
P: Packet<'a>,
{
use std::io::Read;
use flate2::bufread::ZlibEncoder;
use flate2::Compression;
let start_len = buf.len();
pkt.encode_packet(&mut *buf)?;
let data_len = buf.len() - start_len;
if data_len > threshold as usize {
let mut z = ZlibEncoder::new(&buf[start_len..], Compression::new(4));
scratch.clear();
let data_len_size = VarInt(data_len as i32).written_size();
let packet_len = data_len_size + z.read_to_end(scratch)?;
ensure!(
packet_len <= MAX_PACKET_SIZE as usize,
"packet exceeds maximum length"
);
drop(z);
buf.truncate(start_len);
VarInt(packet_len as i32).encode(&mut *buf)?;
VarInt(data_len as i32).encode(&mut *buf)?;
buf.extend_from_slice(scratch);
} else {
let data_len_size = 1;
let packet_len = data_len_size + data_len;
ensure!(
packet_len <= MAX_PACKET_SIZE as usize,
"packet exceeds maximum length"
);
let packet_len_size = VarInt(packet_len as i32).written_size();
let data_prefix_len = packet_len_size + data_len_size;
buf.put_bytes(0, data_prefix_len);
buf.copy_within(start_len..start_len + data_len, start_len + data_prefix_len);
let mut front = &mut buf[start_len..];
VarInt(packet_len as i32).encode(&mut front)?;
// Zero for no compression on this packet.
VarInt(0).encode(front)?;
}
Ok(())
}

View file

@ -2,10 +2,10 @@ use std::borrow::Cow;
use std::collections::{BTreeSet, HashSet};
use std::hash::{BuildHasher, Hash};
use std::io::Write;
use std::mem;
use std::mem::MaybeUninit;
use std::rc::Rc;
use std::sync::Arc;
use std::{io, mem};
use anyhow::ensure;
use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
@ -22,20 +22,20 @@ impl Encode for bool {
Ok(w.write_u8(*self as u8)?)
}
fn write_slice(slice: &[bool], mut w: impl Write) -> io::Result<()> {
fn encode_slice(slice: &[bool], mut w: impl Write) -> Result<()> {
// SAFETY: Bools have the same layout as u8.
// Bools are guaranteed to have the correct bit pattern.
let bytes: &[u8] = unsafe { mem::transmute(slice) };
w.write_all(bytes)
Ok(w.write_all(bytes)?)
}
const HAS_WRITE_SLICE: bool = true;
const HAS_ENCODE_SLICE: bool = true;
}
impl Decode<'_> for bool {
fn decode(r: &mut &[u8]) -> Result<Self> {
let n = r.read_u8()?;
ensure!(n <= 1, "decoded boolean is not 0 or 1");
ensure!(n <= 1, "decoded boolean is not 0 or 1 (got {n})");
Ok(n == 1)
}
}
@ -45,11 +45,11 @@ impl Encode for u8 {
Ok(w.write_u8(*self)?)
}
fn write_slice(slice: &[u8], mut w: impl Write) -> io::Result<()> {
w.write_all(slice)
fn encode_slice(slice: &[u8], mut w: impl Write) -> Result<()> {
Ok(w.write_all(slice)?)
}
const HAS_WRITE_SLICE: bool = true;
const HAS_ENCODE_SLICE: bool = true;
}
impl Decode<'_> for u8 {
@ -63,15 +63,13 @@ impl Encode for i8 {
Ok(w.write_i8(*self)?)
}
fn write_slice(slice: &[i8], mut w: impl Write) -> io::Result<()>
where
Self: Sized,
{
fn encode_slice(slice: &[i8], mut w: impl Write) -> Result<()> {
// SAFETY: i8 has the same layout as u8.
let bytes: &[u8] = unsafe { mem::transmute(slice) };
w.write_all(bytes)
Ok(w.write_all(bytes)?)
}
const HAS_WRITE_SLICE: bool = true;
const HAS_ENCODE_SLICE: bool = true;
}
impl Decode<'_> for i8 {
@ -455,8 +453,8 @@ impl_tuple!(A B C D E F G H I J K L);
/// Like tuples, arrays are encoded and decoded without a VarInt length prefix.
impl<const N: usize, T: Encode> Encode for [T; N] {
fn encode(&self, mut w: impl Write) -> Result<()> {
if T::HAS_WRITE_SLICE {
return Ok(T::write_slice(self, w)?);
if T::HAS_ENCODE_SLICE {
return T::encode_slice(self, w);
}
for t in self {
@ -519,8 +517,8 @@ impl<T: Encode> Encode for [T] {
VarInt(len as i32).encode(&mut w)?;
if T::HAS_WRITE_SLICE {
return Ok(T::write_slice(self, w)?);
if T::HAS_ENCODE_SLICE {
return T::encode_slice(self, w);
}
for t in self {

View file

@ -20,6 +20,7 @@ pub const STACK_MIN: u8 = 1;
pub const STACK_MAX: u8 = 127;
impl ItemStack {
#[must_use]
pub fn new(item: ItemKind, count: u8, nbt: Option<Compound>) -> Self {
Self {
item,
@ -28,6 +29,24 @@ impl ItemStack {
}
}
#[must_use]
pub fn with_count(mut self, count: u8) -> Self {
self.set_count(count);
self
}
#[must_use]
pub fn with_item(mut self, item: ItemKind) -> Self {
self.item = item;
self
}
#[must_use]
pub fn with_nbt(mut self, nbt: impl Into<Option<Compound>>) -> Self {
self.nbt = nbt.into();
self
}
/// Gets the number of items in this stack.
pub fn count(&self) -> u8 {
self.count

View file

@ -5,14 +5,16 @@
//! and serverbound packets are defined in the [`packet`] module. Packets are
//! encoded and decoded using the [`PacketEncoder`] and [`PacketDecoder`] types.
//!
//! [`PacketEncoder`]: codec::PacketEncoder
//! [`PacketDecoder`]: codec::PacketDecoder
//! [`PacketEncoder`]: encoder::PacketEncoder
//! [`PacketDecoder`]: decoder::PacketDecoder
//!
//! # Examples
//!
//! ```
//! use valence_protocol::codec::{PacketDecoder, PacketEncoder};
//! use valence_protocol::decoder::PacketDecoder;
//! use valence_protocol::encoder::PacketEncoder;
//! use valence_protocol::packet::c2s::play::RenameItemC2s;
//! use valence_protocol::Packet;
//!
//! let mut enc = PacketEncoder::new();
//!
@ -26,7 +28,9 @@
//!
//! dec.queue_bytes(enc.take());
//!
//! let incoming = dec.try_next_packet::<RenameItemC2s>().unwrap().unwrap();
//! let frame = dec.try_next_packet().unwrap().unwrap();
//!
//! let incoming = RenameItemC2s::decode_packet(&mut &frame[..]).unwrap();
//!
//! assert_eq!(outgoing.item_name, incoming.item_name);
//! ```
@ -67,12 +71,12 @@
// Allows us to use our own proc macros internally.
extern crate self as valence_protocol;
use std::fmt;
use std::io::Write;
use std::{fmt, io};
pub use anyhow::{Error, Result};
pub use valence_protocol_macros::{ident, Decode, Encode, Packet};
pub use {uuid, valence_nbt as nbt};
pub use {bytes, uuid, valence_nbt as nbt};
/// The Minecraft protocol version this library currently targets.
pub const PROTOCOL_VERSION: i32 = 762;
@ -85,8 +89,9 @@ pub mod array;
pub mod block;
pub mod block_pos;
pub mod byte_angle;
pub mod codec;
pub mod decoder;
pub mod enchant;
pub mod encoder;
pub mod ident;
mod impls;
pub mod item;
@ -172,17 +177,17 @@ pub trait Encode {
/// Hack to get around the lack of specialization. Not public API.
#[doc(hidden)]
fn write_slice(slice: &[Self], w: impl Write) -> io::Result<()>
fn encode_slice(slice: &[Self], w: impl Write) -> Result<()>
where
Self: Sized,
{
let _ = (slice, w);
unimplemented!("for internal use in valence_protocol only")
unimplemented!("no implementation of `encode_slice`")
}
/// Hack to get around the lack of specialization. Not public API.
#[doc(hidden)]
const HAS_WRITE_SLICE: bool = false;
const HAS_ENCODE_SLICE: bool = false;
}
/// The `Decode` trait allows objects to be read from the Minecraft protocol. It
@ -296,7 +301,13 @@ pub trait Packet<'a>: Sized + fmt::Debug {
#[allow(dead_code)]
#[cfg(test)]
mod tests {
use std::borrow::Cow;
use bytes::BytesMut;
use super::*;
use crate::decoder::{decode_packet, PacketDecoder};
use crate::encoder::PacketEncoder;
use crate::packet::c2s::play::HandSwingC2s;
use crate::packet::C2sPlayPacket;
@ -393,4 +404,101 @@ mod tests {
"HandSwingC2s"
);
}
use crate::block_pos::BlockPos;
use crate::ident::Ident;
use crate::item::{ItemKind, ItemStack};
use crate::text::{Text, TextFormat};
use crate::types::Hand;
use crate::var_int::VarInt;
use crate::var_long::VarLong;
#[cfg(feature = "encryption")]
const CRYPT_KEY: [u8; 16] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16];
#[derive(PartialEq, Debug, Encode, Decode, Packet)]
#[packet_id = 42]
struct TestPacket<'a> {
a: bool,
b: u8,
c: i32,
d: f32,
e: f64,
f: BlockPos,
g: Hand,
h: Ident<Cow<'a, str>>,
i: Option<ItemStack>,
j: Text,
k: VarInt,
l: VarLong,
m: &'a str,
n: &'a [u8; 10],
o: [u128; 3],
}
impl<'a> TestPacket<'a> {
fn new(string: &'a str) -> Self {
Self {
a: true,
b: 12,
c: -999,
d: 5.001,
e: 1e10,
f: BlockPos::new(1, 2, 3),
g: Hand::Off,
h: Ident::new("minecraft:whatever").unwrap(),
i: Some(ItemStack::new(ItemKind::WoodenSword, 12, None)),
j: "my ".into_text() + "fancy".italic() + " text",
k: VarInt(123),
l: VarLong(456),
m: string,
n: &[7; 10],
o: [123456789; 3],
}
}
}
fn check_test_packet(dec: &mut PacketDecoder, string: &str) {
let frame = dec.try_next_packet().unwrap().unwrap();
let pkt = decode_packet::<TestPacket>(&frame).unwrap();
assert_eq!(&pkt, &TestPacket::new(string));
}
#[test]
fn packets_round_trip() {
let mut buf = BytesMut::new();
let mut enc = PacketEncoder::new();
enc.append_packet(&TestPacket::new("first")).unwrap();
#[cfg(feature = "compression")]
enc.set_compression(Some(0));
enc.append_packet(&TestPacket::new("second")).unwrap();
buf.unsplit(enc.take());
#[cfg(feature = "encryption")]
enc.enable_encryption(&CRYPT_KEY);
enc.append_packet(&TestPacket::new("third")).unwrap();
enc.prepend_packet(&TestPacket::new("fourth")).unwrap();
buf.unsplit(enc.take());
let mut dec = PacketDecoder::new();
dec.queue_bytes(buf);
check_test_packet(&mut dec, "first");
#[cfg(feature = "compression")]
dec.set_compression(Some(0));
check_test_packet(&mut dec, "second");
#[cfg(feature = "encryption")]
dec.enable_encryption(&CRYPT_KEY);
check_test_packet(&mut dec, "fourth");
check_test_packet(&mut dec, "third");
}
}

View file

@ -11,7 +11,7 @@ pub struct ClickSlotC2s {
/// because the meaning of this value depends on the mode.
pub button: i8,
pub mode: ClickMode,
pub slots: Vec<Slot>,
pub slot_changes: Vec<Slot>,
pub carried_item: Option<ItemStack>,
}

View file

@ -14,10 +14,11 @@ pub struct ClientSettingsC2s<'a> {
pub allow_server_listings: bool,
}
#[derive(Copy, Clone, PartialEq, Eq, Debug, Encode, Decode)]
#[derive(Copy, Clone, PartialEq, Eq, Default, Debug, Encode, Decode)]
pub enum ChatMode {
Enabled,
CommandsOnly,
#[default]
Hidden,
}

View file

@ -18,7 +18,7 @@ impl VarInt {
/// Returns the exact number of bytes this varint will write when
/// [`Encode::encode`] is called, assuming no error occurs.
pub fn written_size(self) -> usize {
pub const fn written_size(self) -> usize {
match self.0 {
0 => 1,
n => (31 - n.leading_zeros() as usize) / 7 + 1,

View file

@ -1,5 +1,5 @@
//! This crate provides derive macros for [`Encode`], [`Decode`], and
//! [`Packet`]. It also provides the procedural macro [`ident_str!`] for parsing
//! [`Packet`]. It also provides the procedural macro [`ident!`] for parsing
//! identifiers at compile time.
//!
//! See `valence_protocol`'s documentation for more information.

View file

@ -5,7 +5,8 @@ use anyhow::bail;
use tokio::io::AsyncWriteExt;
use tokio::net::TcpStream;
use uuid::Uuid;
use valence_protocol::codec::{PacketDecoder, PacketEncoder};
use valence_protocol::decoder::{decode_packet, PacketDecoder};
use valence_protocol::encoder::PacketEncoder;
use valence_protocol::packet::c2s::handshake::handshake::NextState;
use valence_protocol::packet::c2s::handshake::HandshakeC2s;
use valence_protocol::packet::c2s::login::LoginHelloC2s;
@ -76,24 +77,26 @@ pub async fn make_session<'a>(params: &SessionParams<'a>) -> anyhow::Result<()>
dec.queue_bytes(read_buf);
if let Ok(Some(pkt)) = dec.try_next_packet::<S2cLoginPacket>() {
match pkt {
S2cLoginPacket::LoginCompressionS2c(p) => {
let threshold = p.threshold.0 as u32;
if let Ok(Some(frame)) = dec.try_next_packet() {
if let Ok(pkt) = decode_packet::<S2cLoginPacket>(&frame) {
match pkt {
S2cLoginPacket::LoginCompressionS2c(p) => {
let threshold = p.threshold.0 as u32;
dec.set_compression(true);
enc.set_compression(Some(threshold));
dec.set_compression(Some(threshold));
enc.set_compression(Some(threshold));
}
S2cLoginPacket::LoginSuccessS2c(_) => {
break;
}
S2cLoginPacket::LoginHelloS2c(_) => {
bail!("encryption not implemented");
}
_ => (),
}
S2cLoginPacket::LoginSuccessS2c(_) => {
break;
}
S2cLoginPacket::LoginHelloS2c(_) => {
bail!("encryption not implemented");
}
_ => (),
}
}
}
@ -101,26 +104,8 @@ pub async fn make_session<'a>(params: &SessionParams<'a>) -> anyhow::Result<()>
println!("{sess_name} logged in");
loop {
while !dec.has_next_packet()? {
dec.reserve(rb_size);
let mut read_buf = dec.take_capacity();
conn.readable().await?;
match conn.try_read_buf(&mut read_buf) {
Ok(0) => return Err(io::Error::from(ErrorKind::UnexpectedEof).into()),
Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => continue,
Err(e) => return Err(e.into()),
Ok(_) => (),
};
dec.queue_bytes(read_buf);
}
match dec.try_next_packet::<S2cPlayPacket>() {
Ok(None) => continue,
Ok(Some(pkt)) => match pkt {
while let Some(frame) = dec.try_next_packet()? {
match decode_packet(&frame)? {
S2cPlayPacket::KeepAliveS2c(p) => {
enc.clear();
@ -143,8 +128,22 @@ pub async fn make_session<'a>(params: &SessionParams<'a>) -> anyhow::Result<()>
conn.write_all(&enc.take()).await?;
}
_ => (),
},
Err(err) => return Err(err),
}
}
dec.reserve(rb_size);
let mut read_buf = dec.take_capacity();
conn.readable().await?;
match conn.try_read_buf(&mut read_buf) {
Ok(0) => return Err(io::Error::from(ErrorKind::UnexpectedEof).into()),
Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => continue,
Err(e) => return Err(e.into()),
Ok(_) => (),
};
dec.queue_bytes(read_buf);
}
}