From 11ba70586e46729e0f45606e9495c3ad53681db7 Mon Sep 17 00:00:00 2001 From: Ryan Johnson Date: Sat, 8 Apr 2023 12:55:31 -0700 Subject: [PATCH] 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. --- crates/packet_inspector/src/context.rs | 198 +-- crates/packet_inspector/src/main.rs | 10 +- crates/packet_inspector/src/state.rs | 64 +- crates/playground/src/extras.rs | 21 +- crates/playground/src/main.rs | 5 +- crates/playground/src/playground.template.rs | 11 +- crates/valence/examples/bench_players.rs | 17 +- crates/valence/examples/biomes.rs | 7 +- crates/valence/examples/block_entities.rs | 32 +- crates/valence/examples/building.rs | 67 +- crates/valence/examples/chest.rs | 51 +- crates/valence/examples/combat.rs | 55 +- crates/valence/examples/conway.rs | 48 +- crates/valence/examples/cow_sphere.rs | 26 +- crates/valence/examples/death.rs | 42 +- crates/valence/examples/parkour.rs | 26 +- crates/valence/examples/particles.rs | 17 +- crates/valence/examples/player_list.rs | 2 - crates/valence/examples/resource_pack.rs | 66 +- crates/valence/examples/terrain.rs | 17 +- crates/valence/examples/text.rs | 2 - crates/valence/src/client.rs | 350 ++-- crates/valence/src/client/action.rs | 105 ++ crates/valence/src/client/command.rs | 134 ++ .../src/client/default_event_handler.rs | 116 -- crates/valence/src/client/event.rs | 1459 ----------------- crates/valence/src/client/interact_entity.rs | 49 + crates/valence/src/client/keepalive.rs | 85 + crates/valence/src/client/misc.rs | 150 ++ crates/valence/src/client/movement.rs | 210 +++ crates/valence/src/client/settings.rs | 53 + crates/valence/src/client/teleport.rs | 128 ++ crates/valence/src/component.rs | 2 +- crates/valence/src/entity.rs | 4 +- crates/valence/src/event_loop.rs | 177 ++ crates/valence/src/inventory.rs | 640 +++++--- crates/valence/src/inventory/validate.rs | 202 +-- crates/valence/src/lib.rs | 12 +- crates/valence/src/packet.rs | 2 +- crates/valence/src/player_list.rs | 3 +- crates/valence/src/server.rs | 22 +- crates/valence/src/server/connect.rs | 5 +- crates/valence/src/server/connection.rs | 194 ++- crates/valence/src/unit_test/example.rs | 6 +- crates/valence/src/unit_test/util.rs | 107 +- crates/valence/src/weather.rs | 14 +- .../valence_anvil/examples/anvil_loading.rs | 17 +- crates/valence_protocol/benches/benches.rs | 21 +- crates/valence_protocol/src/codec.rs | 625 ------- crates/valence_protocol/src/decoder.rs | 183 +++ crates/valence_protocol/src/encoder.rs | 270 +++ crates/valence_protocol/src/impls.rs | 34 +- crates/valence_protocol/src/item.rs | 19 + crates/valence_protocol/src/lib.rs | 128 +- .../src/packet/c2s/play/click_slot.rs | 2 +- .../src/packet/c2s/play/client_settings.rs | 3 +- crates/valence_protocol/src/var_int.rs | 2 +- crates/valence_protocol_macros/src/lib.rs | 2 +- crates/valence_stresser/src/stresser.rs | 77 +- 59 files changed, 2875 insertions(+), 3521 deletions(-) create mode 100644 crates/valence/src/client/action.rs create mode 100644 crates/valence/src/client/command.rs delete mode 100644 crates/valence/src/client/default_event_handler.rs delete mode 100644 crates/valence/src/client/event.rs create mode 100644 crates/valence/src/client/interact_entity.rs create mode 100644 crates/valence/src/client/keepalive.rs create mode 100644 crates/valence/src/client/misc.rs create mode 100644 crates/valence/src/client/movement.rs create mode 100644 crates/valence/src/client/settings.rs create mode 100644 crates/valence/src/client/teleport.rs create mode 100644 crates/valence/src/event_loop.rs delete mode 100644 crates/valence_protocol/src/codec.rs create mode 100644 crates/valence_protocol/src/decoder.rs create mode 100644 crates/valence_protocol/src/encoder.rs diff --git a/crates/packet_inspector/src/context.rs b/crates/packet_inspector/src/context.rs index c61c28d..f1dc7e4 100644 --- a/crates/packet_inspector/src/context.rs +++ b/crates/packet_inspector/src/context.rs @@ -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, pub(crate) packet_data: Vec, pub(crate) stage: Stage, pub(crate) packet_type: i32, @@ -93,169 +92,58 @@ impl Packet { pub fn get_raw_packet(&self) -> Vec { 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::() { - 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::() { - 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::() { - 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::() { - 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::() { - 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::() { - 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::() { - 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::() { - 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::() { - 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::() { - 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::() { - 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::() { - 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), } } } diff --git a/crates/packet_inspector/src/main.rs b/crates/packet_inspector/src/main.rs index 3f783cc..14142ae 100644 --- a/crates/packet_inspector/src/main.rs +++ b/crates/packet_inspector/src/main.rs @@ -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::(Stage::LoginSuccessS2c) .await?; diff --git a/crates/packet_inspector/src/state.rs b/crates/packet_inspector/src/state.rs index 3cb2556..26c7f8c 100644 --- a/crates/packet_inspector/src/state.rs +++ b/crates/packet_inspector/src/state.rs @@ -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, 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) } } diff --git a/crates/playground/src/extras.rs b/crates/playground/src/extras.rs index 9a1aa86..d1e44d6 100644 --- a/crates/playground/src/extras.rs +++ b/crates/playground/src/extras.rs @@ -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, + mut events: EventReader, ) { for event in events.iter() { - let Ok(mut mode) = clients.get_component_mut::(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, + }; + } + } } } diff --git a/crates/playground/src/main.rs b/crates/playground/src/main.rs index ad21c8d..54ab3c6 100644 --- a/crates/playground/src/main.rs +++ b/crates/playground/src/main.rs @@ -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); diff --git a/crates/playground/src/playground.template.rs b/crates/playground/src/playground.template.rs index 76f592f..f74426d 100644 --- a/crates/playground/src/playground.template.rs +++ b/crates/playground/src/playground.template.rs @@ -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>, + mut clients: Query<(&mut Location, &mut Position), Added>, instances: Query>, ) { - 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! diff --git a/crates/valence/examples/bench_players.rs b/crates/valence/examples/bench_players.rs index 6383137..2a9841f 100644 --- a/crates/valence/examples/bench_players.rs +++ b/crates/valence/examples/bench_players.rs @@ -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>, + mut clients: Query<(&mut Location, &mut Position, &mut GameMode), Added>, instances: Query>, - 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() - }); } } diff --git a/crates/valence/examples/biomes.rs b/crates/valence/examples/biomes.rs index 2f283fc..6b35186 100644 --- a/crates/valence/examples/biomes.rs +++ b/crates/valence/examples/biomes.rs @@ -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(); } diff --git a/crates/valence/examples/block_entities.rs b/crates/valence/examples/block_entities.rs index f336b6b..7d4d17e 100644 --- a/crates/valence/examples/block_entities.rs +++ b/crates/valence/examples/block_entities.rs @@ -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>, + mut clients: Query<(&mut Location, &mut Position, &mut Look, &mut GameMode), Added>, instances: Query>, - 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, - mut block_interacts: EventReader, + mut block_interacts: EventReader, 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, diff --git a/crates/valence/examples/building.rs b/crates/valence/examples/building.rs index 1f6c4f5..fe1bf98 100644 --- a/crates/valence/examples/building.rs +++ b/crates/valence/examples/building.rs @@ -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>, + mut clients: Query<(&mut Client, &mut Location, &mut Position, &mut GameMode), Added>, instances: Query>, - 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, -) { +fn toggle_gamemode_on_sneak(mut clients: Query<&mut GameMode>, mut events: EventReader) { 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, + mut events: EventReader, ) { 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, + mut events: EventReader, ) { 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, + mut events: EventReader, ) { 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()); } } diff --git a/crates/valence/examples/chest.rs b/crates/valence/examples/chest.rs index dd30d91..7422410 100644 --- a/crates/valence/examples/chest.rs +++ b/crates/valence/examples/chest.rs @@ -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>, + mut clients: Query<(&mut Location, &mut Position, &mut GameMode), Added>, instances: Query>, - 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, -) { +fn toggle_gamemode_on_sneak(mut clients: Query<&mut GameMode>, mut events: EventReader) { 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, Without)>, - mut events: EventReader, + mut events: EventReader, ) { - 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); } } diff --git a/crates/valence/examples/combat.rs b/crates/valence/examples/combat.rs index 0d97e22..06766e3 100644 --- a/crates/valence/examples/combat.rs +++ b/crates/valence/examples/combat.rs @@ -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>, + mut clients: Query<(Entity, &mut Location, &mut Position), Added>, instances: Query>, 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, server: Res, mut clients: Query, - mut start_sprinting: EventReader, - mut stop_sprinting: EventReader, - mut interact_with_entity: EventReader, + mut sprinting: EventReader, + mut interact_entity: EventReader, ) { - 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 diff --git a/crates/valence/examples/conway.rs b/crates/valence/examples/conway.rs index 723152d..466f20a 100644 --- a/crates/valence/examples/conway.rs +++ b/crates/valence/examples/conway.rs @@ -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>, + mut clients: Query<(&mut Client, &mut Location, &mut Position), Added>, instances: Query>, - 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, mut board: ResMut) { +fn toggle_cell_on_dig(mut events: EventReader, mut board: ResMut) { 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, + mut events: EventReader, mut board: ResMut, 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)); + } } } } diff --git a/crates/valence/examples/cow_sphere.rs b/crates/valence/examples/cow_sphere.rs index b3abea7..0e0e6ee 100644 --- a/crates/valence/examples/cow_sphere.rs +++ b/crates/valence/examples/cow_sphere.rs @@ -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>, + mut clients: Query<(&mut Location, &mut Position, &mut GameMode), Added>, instances: Query>, - 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; } } diff --git a/crates/valence/examples/death.rs b/crates/valence/examples/death.rs index 04af337..618882e 100644 --- a/crates/valence/examples/death.rs +++ b/crates/valence/examples/death.rs @@ -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>, + mut clients: Query< + ( + &mut Client, + &mut Location, + &mut Position, + &mut HasRespawnScreen, + ), + Added, + >, instances: Query>, - 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) { +fn squat_and_die(mut clients: Query<&mut Client>, mut events: EventReader) { 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, + mut events: EventReader, instances: Query>, ) { for event in events.iter() { diff --git a/crates/valence/examples/parkour.rs b/crates/valence/examples/parkour.rs index 53adec2..02dd275 100644 --- a/crates/valence/examples/parkour.rs +++ b/crates/valence/examples/parkour.rs @@ -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>, + mut clients: Query< + ( + Entity, + &mut Client, + &mut Location, + &mut IsFlat, + &mut GameMode, + ), + Added, + >, server: Res, 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)); } } diff --git a/crates/valence/examples/particles.rs b/crates/valence/examples/particles.rs index c76add5..c294102 100644 --- a/crates/valence/examples/particles.rs +++ b/crates/valence/examples/particles.rs @@ -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>, + mut clients: Query<(&mut Location, &mut Position, &mut GameMode), Added>, instances: Query>, - 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() - }); } } diff --git a/crates/valence/examples/player_list.rs b/crates/valence/examples/player_list.rs index 1dacb60..c9597fd 100644 --- a/crates/valence/examples/player_list.rs +++ b/crates/valence/examples/player_list.rs @@ -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, diff --git a/crates/valence/examples/resource_pack.rs b/crates/valence/examples/resource_pack.rs index 5afe336..a6c8a70 100644 --- a/crates/valence/examples/resource_pack.rs +++ b/crates/valence/examples/resource_pack.rs @@ -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) { +fn prompt_on_punch(mut clients: Query<&mut Client>, mut events: EventReader) { 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, ) { 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)); - } - } } } diff --git a/crates/valence/examples/terrain.rs b/crates/valence/examples/terrain.rs index ceff652..0078590 100644 --- a/crates/valence/examples/terrain.rs +++ b/crates/valence/examples/terrain.rs @@ -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>, + mut clients: Query<(&mut Location, &mut Position, &mut IsFlat, &mut GameMode), Added>, instances: Query>, - 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() - }); } } diff --git a/crates/valence/examples/text.rs b/crates/valence/examples/text.rs index 60b097c..2fc54a1 100644 --- a/crates/valence/examples/text.rs +++ b/crates/valence/examples/text.rs @@ -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(); diff --git a/crates/valence/src/client.rs b/crates/valence/src/client.rs index 79bba1a..bfffc55 100644 --- a/crates/valence/src/client.rs +++ b/crates/valence/src/client.rs @@ -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, 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, 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; + /// Receives the next pending serverbound packet. This must return + /// immediately without blocking. + fn try_recv(&mut self) -> anyhow::Result>; + /// 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, 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); // 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, @@ -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, Changed)>, - >, -) { - 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>, ) { @@ -1243,44 +1205,6 @@ fn update_op_level(mut clients: Query<(&mut Client, &OpLevel), Changed> } } -fn acknowledge_player_actions( - mut clients: Query<(&mut Client, &mut PlayerActionSequence), Changed>, -) { - 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, - 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::(); - } - } - } -} - #[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, .. diff --git a/crates/valence/src/client/action.rs b/crates/valence/src/client/action.rs new file mode 100644 index 0000000..dc15f40 --- /dev/null +++ b/crates/valence/src/client/action.rs @@ -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::() + .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, + mut digging_events: EventWriter, +) { + for packet in packets.iter() { + if let Some(pkt) = packet.decode::() { + 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>, +) { + 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; + } + } +} diff --git a/crates/valence/src/client/command.rs b/crates/valence/src/client/command.rs new file mode 100644 index 0000000..59fd693 --- /dev/null +++ b/crates/valence/src/client/command.rs @@ -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::() + .add_event::() + .add_event::() + .add_event::() + .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, + mut clients: Query<&mut Flags>, + mut sprinting_events: EventWriter, + mut sneaking_events: EventWriter, + mut jump_with_horse_events: EventWriter, + mut leave_bed_events: EventWriter, +) { + for packet in packets.iter() { + if let Some(pkt) = packet.decode::() { + 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 + } + } + } +} diff --git a/crates/valence/src/client/default_event_handler.rs b/crates/valence/src/client/default_event_handler.rs deleted file mode 100644 index 84464c8..0000000 --- a/crates/valence/src/client/default_event_handler.rs +++ /dev/null @@ -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, - mut update_settings_events: EventReader, - mut player_move: EventReader, - mut start_sneaking: EventReader, - mut stop_sneaking: EventReader, - mut start_sprinting: EventReader, - mut stop_sprinting: EventReader, - mut swing_arm: EventReader, -) { - 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, - }); - } - } - } -} diff --git a/crates/valence/src/client/event.rs b/crates/valence/src/client/event.rs deleted file mode 100644 index c260471..0000000 --- a/crates/valence/src/client/event.rs +++ /dev/null @@ -1,1459 +0,0 @@ -use std::borrow::Cow; -use std::cmp; - -use anyhow::bail; -use bevy_app::{CoreSet, Plugin}; -use bevy_ecs::prelude::*; -use bevy_ecs::query::WorldQuery; -use bevy_ecs::schedule::ScheduleLabel; -use bevy_ecs::system::{SystemParam, SystemState}; -use glam::{DVec3, Vec3}; -use paste::paste; -use tracing::{debug, warn}; -use uuid::Uuid; -use valence_protocol::block_pos::BlockPos; -use valence_protocol::ident::Ident; -use valence_protocol::item::ItemStack; -use valence_protocol::packet::c2s::play::click_slot::{ClickMode, Slot}; -use valence_protocol::packet::c2s::play::client_command::Action as ClientCommandAction; -use valence_protocol::packet::c2s::play::client_settings::{ChatMode, DisplayedSkinParts, MainArm}; -use valence_protocol::packet::c2s::play::player_action::Action as PlayerAction; -use valence_protocol::packet::c2s::play::player_interact_entity::EntityInteraction; -use valence_protocol::packet::c2s::play::recipe_category_options::RecipeBookId; -use valence_protocol::packet::c2s::play::update_command_block::Mode as CommandBlockMode; -use valence_protocol::packet::c2s::play::update_structure_block::{ - Action as StructureBlockAction, Flags as StructureBlockFlags, Mirror as StructureBlockMirror, - Mode as StructureBlockMode, Rotation as StructureBlockRotation, -}; -use valence_protocol::packet::c2s::play::{ - AdvancementTabC2s, ClientStatusC2s, ResourcePackStatusC2s, UpdatePlayerAbilitiesC2s, -}; -use valence_protocol::packet::s2c::play::InventoryS2c; -use valence_protocol::packet::C2sPlayPacket; -use valence_protocol::types::{Difficulty, Direction, Hand}; -use valence_protocol::var_int::VarInt; - -use super::{ - CursorItem, KeepaliveState, PlayerActionSequence, PlayerInventoryState, TeleportState, -}; -use crate::client::Client; -use crate::component::{Look, OnGround, Ping, Position}; -use crate::inventory::{Inventory, InventorySettings}; -use crate::packet::WritePacket; -use crate::prelude::OpenInventory; - -#[derive(Clone, Debug)] -pub struct QueryBlockNbt { - pub client: Entity, - pub position: BlockPos, - pub transaction_id: i32, -} - -#[derive(Clone, Debug)] -pub struct UpdateDifficulty { - pub client: Entity, - pub difficulty: Difficulty, -} - -#[derive(Clone, Debug)] -pub struct MessageAcknowledgment { - pub client: Entity, - pub message_count: i32, -} - -#[derive(Clone, Debug)] -pub struct CommandExecution { - pub client: Entity, - pub command: Box, - pub timestamp: u64, -} - -#[derive(Clone, Debug)] -pub struct ChatMessage { - pub client: Entity, - pub message: Box, - pub timestamp: u64, -} - -#[derive(Clone, Debug)] -pub struct PerformRespawn { - pub client: Entity, -} - -#[derive(Clone, Debug)] -pub struct RequestStats { - pub client: Entity, -} - -#[derive(Clone, Debug)] -pub struct ClientSettings { - pub client: Entity, - /// e.g. en_US - pub locale: Box, - /// The client side render distance, in chunks. - /// - /// The value is always in `2..=32`. - pub view_distance: u8, - pub chat_mode: ChatMode, - /// `true` if the client has chat colors enabled, `false` otherwise. - pub chat_colors: bool, - pub displayed_skin_parts: DisplayedSkinParts, - pub main_arm: MainArm, - pub enable_text_filtering: bool, - pub allow_server_listings: bool, -} - -#[derive(Clone, Debug)] -pub struct RequestCommandCompletions { - pub client: Entity, - pub transaction_id: i32, - pub text: Box, -} - -#[derive(Clone, Debug)] -pub struct ButtonClick { - pub client: Entity, - pub window_id: i8, - pub button_id: i8, -} - -#[derive(Clone, Debug)] -pub struct ClickSlot { - pub client: Entity, - pub window_id: u8, - pub state_id: i32, - pub slot_id: i16, - pub button: i8, - pub mode: ClickMode, - pub slot_changes: Vec, - pub carried_item: Option, -} - -#[derive(Clone, Debug)] -pub struct CloseHandledScreen { - pub client: Entity, - pub window_id: i8, -} - -#[derive(Clone, Debug)] -pub struct CustomPayload { - pub client: Entity, - pub channel: Ident, - pub data: Box<[u8]>, -} - -#[derive(Clone, Debug)] -pub struct BookUpdate { - pub slot: i32, - pub entries: Vec>, - pub title: Option>, -} - -#[derive(Clone, Debug)] -pub struct QueryEntityNbt { - pub client: Entity, - pub transaction_id: i32, - pub entity_id: i32, -} - -/// Left or right click interaction with an entity's hitbox. -#[derive(Clone, Debug)] -pub struct PlayerInteractEntity { - pub client: Entity, - /// The raw ID of the entity being interacted with. - pub entity_id: i32, - /// If the client was sneaking during the interaction. - pub sneaking: bool, - /// The kind of interaction that occurred. - pub interact: EntityInteraction, -} - -#[derive(Clone, Debug)] -pub struct JigsawGenerating { - pub client: Entity, - pub position: BlockPos, - pub levels: i32, - pub keep_jigsaws: bool, -} - -#[derive(Clone, Debug)] -pub struct UpdateDifficultyLock { - pub client: Entity, - pub locked: bool, -} - -#[derive(Clone, Debug)] -pub struct PlayerMove { - pub client: Entity, - pub position: DVec3, - pub yaw: f32, - pub pitch: f32, - pub on_ground: bool, -} - -#[derive(Clone, Debug)] -pub struct VehicleMove { - pub client: Entity, - pub position: DVec3, - pub yaw: f32, - pub pitch: f32, -} - -#[derive(Clone, Debug)] -pub struct StartSneaking { - pub client: Entity, -} - -#[derive(Clone, Debug)] -pub struct StopSneaking { - pub client: Entity, -} - -#[derive(Clone, Debug)] -pub struct LeaveBed { - pub client: Entity, -} - -#[derive(Clone, Debug)] -pub struct StartSprinting { - pub client: Entity, -} - -#[derive(Clone, Debug)] -pub struct StopSprinting { - pub client: Entity, -} - -#[derive(Clone, Debug)] -pub struct StartJumpWithHorse { - pub client: Entity, - /// The power of the horse jump in `0..=100`. - pub jump_boost: u8, -} - -#[derive(Clone, Debug)] -pub struct StopJumpWithHorse { - pub client: Entity, -} - -#[derive(Clone, Debug)] -pub struct OpenHorseInventory { - pub client: Entity, -} - -#[derive(Clone, Debug)] -pub struct StartFlyingWithElytra { - pub client: Entity, -} - -#[derive(Clone, Debug)] -pub struct BoatPaddleState { - pub client: Entity, - pub left_paddle_turning: bool, - pub right_paddle_turning: bool, -} - -#[derive(Clone, Debug)] -pub struct PickFromInventory { - pub client: Entity, - pub slot_to_use: i32, -} - -#[derive(Clone, Debug)] -pub struct CraftRequest { - pub client: Entity, - pub window_id: i8, - pub recipe: Ident, - pub make_all: bool, -} - -#[derive(Clone, Debug)] -pub struct StopFlying { - pub client: Entity, -} - -#[derive(Clone, Debug)] -pub struct StartFlying { - pub client: Entity, -} - -#[derive(Clone, Debug)] -pub struct StartDigging { - pub client: Entity, - pub position: BlockPos, - pub direction: Direction, - pub sequence: i32, -} - -#[derive(Clone, Debug)] -pub struct AbortDestroyBlock { - pub client: Entity, - pub position: BlockPos, - pub direction: Direction, - pub sequence: i32, -} - -#[derive(Clone, Debug)] -pub struct StopDestroyBlock { - pub client: Entity, - pub position: BlockPos, - pub direction: Direction, - pub sequence: i32, -} - -#[derive(Clone, Debug)] -pub struct DropItemStack { - pub client: Entity, - pub from_slot: Option, - pub stack: ItemStack, -} - -/// Eating food, pulling back bows, using buckets, etc. -#[derive(Clone, Debug)] -pub struct ReleaseUseItem { - pub client: Entity, -} - -#[derive(Clone, Debug)] -pub struct SwapItemWithOffhand { - pub client: Entity, -} - -#[derive(Clone, Debug)] -pub struct PlayerInput { - pub client: Entity, - pub sideways: f32, - pub forward: f32, - pub jump: bool, - pub unmount: bool, -} - -#[derive(Clone, Debug)] -pub struct PlayPong { - pub client: Entity, - pub id: i32, -} - -#[derive(Clone, Debug)] -pub struct PlayerSession { - pub client: Entity, - pub session_id: Uuid, - pub expires_at: i64, - pub public_key_data: Box<[u8]>, - pub key_signature: Box<[u8]>, -} - -#[derive(Clone, Debug)] -pub struct RecipeCategoryOptions { - pub client: Entity, - pub book_id: RecipeBookId, - pub book_open: bool, - pub filter_active: bool, -} - -#[derive(Clone, Debug)] -pub struct RecipeBookData { - pub client: Entity, - pub recipe_id: Ident, -} - -#[derive(Clone, Debug)] -pub struct RenameItem { - pub client: Entity, - pub name: Box, -} - -#[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, -} - -impl From for ResourcePackStatus { - fn from(packet: ResourcePackStatusC2s) -> Self { - match packet { - ResourcePackStatusC2s::Accepted => Self::Accepted, - ResourcePackStatusC2s::Declined => Self::Declined, - ResourcePackStatusC2s::SuccessfullyLoaded => Self::Loaded, - ResourcePackStatusC2s::FailedDownload => Self::FailedDownload, - } - } -} - -#[derive(Clone, Debug)] -pub struct ResourcePackStatusChange { - pub client: Entity, - pub status: ResourcePackStatus, -} - -#[derive(Clone, Debug)] -pub struct OpenAdvancementTab { - pub client: Entity, - pub tab_id: Ident, -} - -#[derive(Clone, Debug)] -pub struct CloseAdvancementScreen { - pub client: Entity, -} - -#[derive(Clone, Debug)] -pub struct SelectMerchantTrade { - pub client: Entity, - pub slot: i32, -} - -#[derive(Clone, Debug)] -pub struct UpdateBeacon { - pub client: Entity, - pub primary_effect: Option, - pub secondary_effect: Option, -} - -#[derive(Clone, Debug)] -pub struct UpdateSelectedSlot { - pub client: Entity, - pub slot: i16, -} - -#[derive(Clone, Debug)] -pub struct UpdateCommandBlock { - pub client: Entity, - pub position: BlockPos, - pub command: Box, - pub mode: CommandBlockMode, - pub track_output: bool, - pub conditional: bool, - pub automatic: bool, -} - -#[derive(Clone, Debug)] -pub struct UpdateCommandBlockMinecart { - pub client: Entity, - pub entity_id: i32, - pub command: Box, - pub track_output: bool, -} - -#[derive(Clone, Debug)] -pub struct CreativeInventoryAction { - pub client: Entity, - pub slot: i16, - pub clicked_item: Option, -} - -#[derive(Clone, Debug)] -pub struct UpdateJigsaw { - pub client: Entity, - pub position: BlockPos, - pub name: Ident, - pub target: Ident, - pub pool: Ident, - pub final_state: Box, - pub joint_type: Box, -} - -#[derive(Clone, Debug)] -pub struct UpdateStructureBlock { - pub client: Entity, - pub position: BlockPos, - pub action: StructureBlockAction, - pub mode: StructureBlockMode, - pub name: Box, - pub offset_xyz: [i8; 3], - pub size_xyz: [i8; 3], - pub mirror: StructureBlockMirror, - pub rotation: StructureBlockRotation, - pub metadata: Box, - pub integrity: f32, - pub seed: i64, - pub flags: StructureBlockFlags, -} - -#[derive(Clone, Debug)] -pub struct UpdateSign { - pub client: Entity, - pub position: BlockPos, - pub lines: [Box; 4], -} - -#[derive(Clone, Debug)] -pub struct HandSwing { - pub client: Entity, - pub hand: Hand, -} - -#[derive(Clone, Debug)] -pub struct SpectatorTeleport { - pub client: Entity, - pub target: Uuid, -} - -#[derive(Clone, Debug)] -pub struct PlayerInteractBlock { - 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 direction: 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 PlayerInteractItem { - pub client: Entity, - pub hand: Hand, - pub sequence: i32, -} - -macro_rules! events { - ( - $( - $group_number:tt { - $($name:ident)* - } - )* - ) => { - /// Inserts [`Events`] resources into the world for each client event. - fn register_client_events(world: &mut World) { - $( - $( - world.insert_resource(Events::<$name>::default()); - )* - )* - } - - paste! { - fn update_all_event_buffers(events: &mut ClientEvents) { - $( - let group = &mut events. $group_number; - $( - group.[< $name:snake >].update(); - )* - )* - } - - pub(crate) type ClientEvents<'w, 's> = ( - $( - [< Group $group_number >]<'w, 's>, - )* - ); - - $( - #[derive(SystemParam)] - pub(crate) struct [< Group $group_number >]<'w, 's> { - $( - [< $name:snake >]: ResMut<'w, Events<$name>>, - )* - #[system_param(ignore)] - _marker: std::marker::PhantomData<&'s ()>, - } - )* - } - } -} - -// Events are grouped to get around the 16 system parameter maximum. -events! { - 0 { - QueryBlockNbt - UpdateDifficulty - MessageAcknowledgment - CommandExecution - ChatMessage - PerformRespawn - RequestStats - ClientSettings - RequestCommandCompletions - ButtonClick - ClickSlot - CloseHandledScreen - CustomPayload - BookUpdate - QueryEntityNbt - } - 1 { - PlayerInteractEntity - JigsawGenerating - UpdateDifficultyLock - PlayerMove - VehicleMove - StartSneaking - StopSneaking - LeaveBed - StartSprinting - StopSprinting - StartJumpWithHorse - StopJumpWithHorse - } - 2 { - OpenHorseInventory - StartFlyingWithElytra - BoatPaddleState - PickFromInventory - CraftRequest - StopFlying - StartFlying - StartDigging - AbortDestroyBlock - StopDestroyBlock - DropItemStack - ReleaseUseItem - SwapItemWithOffhand - PlayerInput - PlayPong - } - 3 { - PlayerSession - RecipeCategoryOptions - RecipeBookData - RenameItem - ResourcePackStatusChange - OpenAdvancementTab - CloseAdvancementScreen - SelectMerchantTrade - UpdateBeacon - UpdateSelectedSlot - UpdateCommandBlock - UpdateCommandBlockMinecart - CreativeInventoryAction - } - 4 { - UpdateJigsaw - UpdateStructureBlock - UpdateSign - HandSwing - SpectatorTeleport - PlayerInteractBlock - PlayerInteractItem - } -} - -pub(crate) struct ClientEventPlugin; - -/// The [`ScheduleLabel`] for the event loop [`Schedule`]. -#[derive(ScheduleLabel, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Debug)] -pub struct EventLoopSchedule; - -/// The default base set for [`EventLoopSchedule`]. -#[derive(SystemSet, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Debug)] -pub struct EventLoopSet; - -impl Plugin for ClientEventPlugin { - fn build(&self, app: &mut bevy_app::App) { - register_client_events(&mut app.world); - - app.configure_set(EventLoopSet.in_base_set(CoreSet::PreUpdate)) - .add_system(run_event_loop.in_set(EventLoopSet)); - - // Add the event loop schedule. - let mut event_loop = Schedule::new(); - event_loop.set_default_base_set(EventLoopSet); - - app.add_schedule(EventLoopSchedule, event_loop); - } -} - -#[derive(WorldQuery)] -#[world_query(mutable)] -pub(crate) struct EventLoopQuery { - entity: Entity, - client: &'static mut Client, - teleport_state: &'static mut TeleportState, - keepalive_state: &'static mut KeepaliveState, - cursor_item: &'static mut CursorItem, - inventory: &'static mut Inventory, - open_inventory: Option<&'static mut OpenInventory>, - position: &'static mut Position, - look: &'static mut Look, - on_ground: &'static mut OnGround, - ping: &'static mut Ping, - player_action_sequence: &'static mut PlayerActionSequence, - player_inventory_state: &'static mut PlayerInventoryState, -} - -/// An exclusive system for running the event loop schedule. -fn run_event_loop( - world: &mut World, - state: &mut SystemState<( - Query, - ClientEvents, - Commands, - Query<&Inventory, Without>, - Res, - )>, - mut clients_to_check: Local>, -) { - let (mut clients, mut events, mut commands, mut inventories, inventory_settings) = - state.get_mut(world); - - update_all_event_buffers(&mut events); - - for mut q in &mut clients { - let Ok(bytes) = q.client.conn.try_recv() else { - // Client is disconnected. - commands.entity(q.entity).remove::(); - continue; - }; - - if bytes.is_empty() { - // No data was received. - continue; - } - - q.client.dec.queue_bytes(bytes); - - match handle_one_packet(&mut q, &mut events, &mut inventories, &inventory_settings) { - Ok(had_packet) => { - if had_packet { - // We decoded one packet, but there might be more. - clients_to_check.push(q.entity); - } - } - Err(e) => { - warn!("failed to dispatch events for client {:?}: {e:?}", q.entity); - commands.entity(q.entity).remove::(); - } - } - } - - state.apply(world); - - // Keep looping until all serverbound packets are decoded. - while !clients_to_check.is_empty() { - world.run_schedule(EventLoopSchedule); - - let (mut clients, mut events, mut commands, mut inventories, inventory_settings) = - state.get_mut(world); - - clients_to_check.retain(|&entity| { - let Ok(mut q) = clients.get_mut(entity) else { - // Client must have been deleted during the last run of the schedule. - return false; - }; - - match handle_one_packet(&mut q, &mut events, &mut inventories, &inventory_settings) { - Ok(had_packet) => had_packet, - Err(e) => { - warn!("failed to dispatch events for client {:?}: {e:?}", q.entity); - commands.entity(entity).remove::(); - false - } - } - }); - - state.apply(world); - } -} - -fn handle_one_packet( - q: &mut EventLoopQueryItem, - events: &mut ClientEvents, - inventories: &mut Query<&Inventory, Without>, - inventory_settings: &Res, -) -> anyhow::Result { - let Some(pkt) = q.client.dec.try_next_packet::()? else { - // No packets to decode. - return Ok(false); - }; - - let entity = q.entity; - - match pkt { - C2sPlayPacket::TeleportConfirmC2s(p) => { - if q.teleport_state.pending_teleports == 0 { - bail!("unexpected teleport confirmation"); - } - - let got = p.teleport_id.0 as u32; - let expected = q - .teleport_state - .teleport_id_counter - .wrapping_sub(q.teleport_state.pending_teleports); - - if got == expected { - q.teleport_state.pending_teleports -= 1; - } else { - bail!("unexpected teleport ID (expected {expected}, got {got}"); - } - } - C2sPlayPacket::QueryBlockNbtC2s(p) => { - events.0.query_block_nbt.send(QueryBlockNbt { - client: entity, - position: p.position, - transaction_id: p.transaction_id.0, - }); - } - C2sPlayPacket::UpdateDifficultyC2s(p) => { - events.0.update_difficulty.send(UpdateDifficulty { - client: entity, - difficulty: p.difficulty, - }); - } - C2sPlayPacket::MessageAcknowledgmentC2s(p) => { - events.0.message_acknowledgment.send(MessageAcknowledgment { - client: entity, - message_count: p.message_count.0, - }); - } - C2sPlayPacket::CommandExecutionC2s(p) => { - events.0.command_execution.send(CommandExecution { - client: entity, - command: p.command.into(), - timestamp: p.timestamp, - }); - } - C2sPlayPacket::ChatMessageC2s(p) => { - events.0.chat_message.send(ChatMessage { - client: entity, - message: p.message.into(), - timestamp: p.timestamp, - }); - } - C2sPlayPacket::ClientStatusC2s(p) => match p { - ClientStatusC2s::PerformRespawn => events - .0 - .perform_respawn - .send(PerformRespawn { client: entity }), - ClientStatusC2s::RequestStats => { - events.0.request_stats.send(RequestStats { client: entity }) - } - }, - C2sPlayPacket::ClientSettingsC2s(p) => { - events.0.client_settings.send(ClientSettings { - client: entity, - locale: p.locale.into(), - view_distance: p.view_distance, - chat_mode: p.chat_mode, - chat_colors: p.chat_colors, - displayed_skin_parts: p.displayed_skin_parts, - main_arm: p.main_arm, - enable_text_filtering: p.enable_text_filtering, - allow_server_listings: p.allow_server_listings, - }); - } - C2sPlayPacket::RequestCommandCompletionsC2s(p) => { - events - .0 - .request_command_completions - .send(RequestCommandCompletions { - client: entity, - transaction_id: p.transaction_id.0, - text: p.text.into(), - }); - } - C2sPlayPacket::ButtonClickC2s(p) => { - events.0.button_click.send(ButtonClick { - client: entity, - window_id: p.window_id, - button_id: p.button_id, - }); - } - C2sPlayPacket::ClickSlotC2s(p) => { - let open_inv = q - .open_inventory - .as_ref() - .and_then(|open| inventories.get_mut(open.entity).ok()); - if let Err(msg) = - crate::inventory::validate_click_slot_impossible(&p, &q.inventory, open_inv) - { - debug!( - "client {:#?} invalid click slot packet: \"{}\" {:#?}", - q.entity, msg, p - ); - let inventory = open_inv.unwrap_or(&q.inventory); - q.client.write_packet(&InventoryS2c { - window_id: if open_inv.is_some() { - q.player_inventory_state.window_id - } else { - 0 - }, - state_id: VarInt(q.player_inventory_state.state_id.0), - slots: Cow::Borrowed(inventory.slot_slice()), - carried_item: Cow::Borrowed(&q.cursor_item.0), - }); - return Ok(true); - } - if inventory_settings.enable_item_dupe_check { - if let Err(msg) = crate::inventory::validate_click_slot_item_duplication( - &p, - &q.inventory, - open_inv, - &q.cursor_item, - ) { - debug!( - "client {:#?} click slot packet tried to incorrectly modify items: \"{}\" \ - {:#?}", - q.entity, msg, p - ); - let inventory = open_inv.unwrap_or(&q.inventory); - q.client.write_packet(&InventoryS2c { - window_id: if open_inv.is_some() { - q.player_inventory_state.window_id - } else { - 0 - }, - state_id: VarInt(q.player_inventory_state.state_id.0), - slots: Cow::Borrowed(inventory.slot_slice()), - carried_item: Cow::Borrowed(&q.cursor_item.0), - }); - return Ok(true); - } - } - if p.slot_idx < 0 && p.mode == ClickMode::Click { - if let Some(stack) = q.cursor_item.0.take() { - events.2.drop_item_stack.send(DropItemStack { - client: entity, - from_slot: None, - stack, - }); - } - } else if p.mode == ClickMode::DropKey { - let entire_stack = p.button == 1; - if let Some(stack) = q.inventory.slot(p.slot_idx as u16) { - let dropped = if entire_stack || stack.count() == 1 { - q.inventory.replace_slot(p.slot_idx as u16, None) - } else { - let mut stack = stack.clone(); - stack.set_count(stack.count() - 1); - let mut old_slot = q.inventory.replace_slot(p.slot_idx as u16, Some(stack)); - // we already checked that the slot was not empty and that the - // stack count is > 1 - old_slot.as_mut().unwrap().set_count(1); - old_slot - } - .expect("dropped item should exist"); // we already checked that the slot was not empty - events.2.drop_item_stack.send(DropItemStack { - client: entity, - from_slot: Some(p.slot_idx as u16), - stack: dropped, - }); - } - } else { - events.0.click_slot.send(ClickSlot { - client: entity, - window_id: p.window_id, - state_id: p.state_id.0, - slot_id: p.slot_idx, - button: p.button, - mode: p.mode, - slot_changes: p.slots, - carried_item: p.carried_item, - }); - } - } - C2sPlayPacket::CloseHandledScreenC2s(p) => { - events.0.close_handled_screen.send(CloseHandledScreen { - client: entity, - window_id: p.window_id, - }); - } - C2sPlayPacket::CustomPayloadC2s(p) => { - events.0.custom_payload.send(CustomPayload { - client: entity, - channel: p.channel.into(), - data: p.data.0.into(), - }); - } - C2sPlayPacket::BookUpdateC2s(p) => { - events.0.book_update.send(BookUpdate { - slot: p.slot.0, - entries: p.entries.into_iter().map(Into::into).collect(), - title: p.title.map(Box::from), - }); - } - C2sPlayPacket::QueryEntityNbtC2s(p) => { - events.0.query_entity_nbt.send(QueryEntityNbt { - client: entity, - transaction_id: p.transaction_id.0, - entity_id: p.entity_id.0, - }); - } - C2sPlayPacket::PlayerInteractEntityC2s(p) => { - events.1.player_interact_entity.send(PlayerInteractEntity { - client: entity, - entity_id: p.entity_id.0, - sneaking: p.sneaking, - interact: p.interact, - }); - } - C2sPlayPacket::JigsawGeneratingC2s(p) => { - events.1.jigsaw_generating.send(JigsawGenerating { - client: entity, - position: p.position, - levels: p.levels.0, - keep_jigsaws: p.keep_jigsaws, - }); - } - C2sPlayPacket::KeepAliveC2s(p) => { - if q.keepalive_state.got_keepalive { - bail!("unexpected keepalive"); - } else if p.id != q.keepalive_state.last_keepalive_id { - bail!( - "keepalive IDs don't match (expected {}, got {})", - q.keepalive_state.last_keepalive_id, - p.id - ); - } else { - q.keepalive_state.got_keepalive = true; - q.ping.0 = q.keepalive_state.keepalive_sent_time.elapsed().as_millis() as i32; - } - } - C2sPlayPacket::UpdateDifficultyLockC2s(p) => { - events.1.update_difficulty_lock.send(UpdateDifficultyLock { - client: entity, - locked: p.locked, - }); - } - C2sPlayPacket::PositionAndOnGround(p) => { - if q.teleport_state.pending_teleports != 0 { - return Ok(false); - } - - events.1.player_move.send(PlayerMove { - client: entity, - position: p.position.into(), - yaw: q.look.yaw, - pitch: q.look.pitch, - on_ground: q.on_ground.0, - }); - - q.position.0 = p.position.into(); - q.teleport_state.synced_pos = p.position.into(); - q.on_ground.0 = p.on_ground; - } - C2sPlayPacket::Full(p) => { - if q.teleport_state.pending_teleports != 0 { - return Ok(false); - } - - events.1.player_move.send(PlayerMove { - client: entity, - position: p.position.into(), - yaw: p.yaw, - pitch: p.pitch, - on_ground: p.on_ground, - }); - - q.position.0 = p.position.into(); - q.teleport_state.synced_pos = p.position.into(); - q.look.yaw = p.yaw; - q.teleport_state.synced_look.yaw = p.yaw; - q.look.pitch = p.pitch; - q.teleport_state.synced_look.pitch = p.pitch; - q.on_ground.0 = p.on_ground; - } - C2sPlayPacket::LookAndOnGround(p) => { - if q.teleport_state.pending_teleports != 0 { - return Ok(false); - } - - events.1.player_move.send(PlayerMove { - client: entity, - position: q.position.0, - yaw: p.yaw, - pitch: p.pitch, - on_ground: p.on_ground, - }); - - q.look.yaw = p.yaw; - q.teleport_state.synced_look.yaw = p.yaw; - q.look.pitch = p.pitch; - q.teleport_state.synced_look.pitch = p.pitch; - q.on_ground.0 = p.on_ground; - } - C2sPlayPacket::OnGroundOnly(p) => { - if q.teleport_state.pending_teleports != 0 { - return Ok(false); - } - - events.1.player_move.send(PlayerMove { - client: entity, - position: q.position.0, - yaw: q.look.yaw, - pitch: q.look.pitch, - on_ground: p.on_ground, - }); - - q.on_ground.0 = p.on_ground; - } - C2sPlayPacket::VehicleMoveC2s(p) => { - if q.teleport_state.pending_teleports != 0 { - return Ok(false); - } - - events.1.vehicle_move.send(VehicleMove { - client: entity, - position: p.position.into(), - yaw: p.yaw, - pitch: p.pitch, - }); - - q.position.0 = p.position.into(); - q.teleport_state.synced_pos = p.position.into(); - q.look.yaw = p.yaw; - q.teleport_state.synced_look.yaw = p.yaw; - q.look.pitch = p.pitch; - q.teleport_state.synced_look.pitch = p.pitch; - } - C2sPlayPacket::BoatPaddleStateC2s(p) => { - events.2.boat_paddle_state.send(BoatPaddleState { - client: entity, - left_paddle_turning: p.left_paddle_turning, - right_paddle_turning: p.right_paddle_turning, - }); - } - C2sPlayPacket::PickFromInventoryC2s(p) => { - events.2.pick_from_inventory.send(PickFromInventory { - client: entity, - slot_to_use: p.slot_to_use.0, - }); - } - C2sPlayPacket::CraftRequestC2s(p) => { - events.2.craft_request.send(CraftRequest { - client: entity, - window_id: p.window_id, - recipe: p.recipe.into(), - make_all: p.make_all, - }); - } - C2sPlayPacket::UpdatePlayerAbilitiesC2s(p) => match p { - UpdatePlayerAbilitiesC2s::StopFlying => { - events.2.stop_flying.send(StopFlying { client: entity }) - } - UpdatePlayerAbilitiesC2s::StartFlying => { - events.2.start_flying.send(StartFlying { client: entity }) - } - }, - C2sPlayPacket::PlayerActionC2s(p) => { - if p.sequence.0 != 0 { - q.player_action_sequence.0 = cmp::max(p.sequence.0, q.player_action_sequence.0); - } - - match p.action { - PlayerAction::StartDestroyBlock => events.2.start_digging.send(StartDigging { - client: entity, - position: p.position, - direction: p.direction, - sequence: p.sequence.0, - }), - PlayerAction::AbortDestroyBlock => { - events.2.abort_destroy_block.send(AbortDestroyBlock { - client: entity, - position: p.position, - direction: p.direction, - sequence: p.sequence.0, - }) - } - PlayerAction::StopDestroyBlock => { - events.2.stop_destroy_block.send(StopDestroyBlock { - client: entity, - position: p.position, - direction: p.direction, - sequence: p.sequence.0, - }) - } - PlayerAction::DropAllItems => { - if let Some(stack) = q - .inventory - .replace_slot(q.player_inventory_state.held_item_slot(), None) - { - q.player_inventory_state.slots_changed |= - 1 << q.player_inventory_state.held_item_slot(); - events.2.drop_item_stack.send(DropItemStack { - client: entity, - from_slot: Some(q.player_inventory_state.held_item_slot()), - stack, - }); - } - } - PlayerAction::DropItem => { - if let Some(stack) = q.inventory.slot(q.player_inventory_state.held_item_slot()) - { - let mut old_slot = if stack.count() == 1 { - q.inventory - .replace_slot(q.player_inventory_state.held_item_slot(), None) - } else { - let mut stack = stack.clone(); - stack.set_count(stack.count() - 1); - q.inventory.replace_slot( - q.player_inventory_state.held_item_slot(), - Some(stack.clone()), - ) - } - .expect("old slot should exist"); // we already checked that the slot was not empty - q.player_inventory_state.slots_changed |= - 1 << q.player_inventory_state.held_item_slot(); - old_slot.set_count(1); - - events.2.drop_item_stack.send(DropItemStack { - client: entity, - from_slot: Some(q.player_inventory_state.held_item_slot()), - stack: old_slot, - }); - } - } - PlayerAction::ReleaseUseItem => events - .2 - .release_use_item - .send(ReleaseUseItem { client: entity }), - PlayerAction::SwapItemWithOffhand => events - .2 - .swap_item_with_offhand - .send(SwapItemWithOffhand { client: entity }), - } - } - C2sPlayPacket::ClientCommandC2s(p) => match p.action { - ClientCommandAction::StartSneaking => events - .1 - .start_sneaking - .send(StartSneaking { client: entity }), - ClientCommandAction::StopSneaking => { - events.1.stop_sneaking.send(StopSneaking { client: entity }) - } - ClientCommandAction::LeaveBed => events.1.leave_bed.send(LeaveBed { client: entity }), - ClientCommandAction::StartSprinting => events - .1 - .start_sprinting - .send(StartSprinting { client: entity }), - ClientCommandAction::StopSprinting => events - .1 - .stop_sprinting - .send(StopSprinting { client: entity }), - ClientCommandAction::StartJumpWithHorse => { - events.1.start_jump_with_horse.send(StartJumpWithHorse { - client: entity, - jump_boost: p.jump_boost.0 as u8, - }) - } - ClientCommandAction::StopJumpWithHorse => events - .1 - .stop_jump_with_horse - .send(StopJumpWithHorse { client: entity }), - ClientCommandAction::OpenHorseInventory => events - .2 - .open_horse_inventory - .send(OpenHorseInventory { client: entity }), - ClientCommandAction::StartFlyingWithElytra => events - .2 - .start_flying_with_elytra - .send(StartFlyingWithElytra { client: entity }), - }, - C2sPlayPacket::PlayerInputC2s(p) => { - events.2.player_input.send(PlayerInput { - client: entity, - sideways: p.sideways, - forward: p.forward, - jump: p.flags.jump(), - unmount: p.flags.unmount(), - }); - } - C2sPlayPacket::PlayPongC2s(p) => { - events.2.play_pong.send(PlayPong { - client: entity, - id: p.id, - }); - } - C2sPlayPacket::PlayerSessionC2s(p) => { - events.3.player_session.send(PlayerSession { - client: entity, - session_id: p.session_id, - expires_at: p.expires_at, - public_key_data: p.public_key_data.into(), - key_signature: p.key_signature.into(), - }); - } - C2sPlayPacket::RecipeCategoryOptionsC2s(p) => { - events - .3 - .recipe_category_options - .send(RecipeCategoryOptions { - client: entity, - book_id: p.book_id, - book_open: p.book_open, - filter_active: p.filter_active, - }); - } - C2sPlayPacket::RecipeBookDataC2s(p) => { - events.3.recipe_book_data.send(RecipeBookData { - client: entity, - recipe_id: p.recipe_id.into(), - }); - } - C2sPlayPacket::RenameItemC2s(p) => { - events.3.rename_item.send(RenameItem { - client: entity, - name: p.item_name.into(), - }); - } - C2sPlayPacket::ResourcePackStatusC2s(p) => { - events - .3 - .resource_pack_status_change - .send(ResourcePackStatusChange { - client: entity, - status: p.into(), - }) - } - C2sPlayPacket::AdvancementTabC2s(p) => match p { - AdvancementTabC2s::OpenedTab { tab_id } => { - events.3.open_advancement_tab.send(OpenAdvancementTab { - client: entity, - tab_id: tab_id.into(), - }) - } - AdvancementTabC2s::ClosedScreen => events - .3 - .close_advancement_screen - .send(CloseAdvancementScreen { client: entity }), - }, - C2sPlayPacket::SelectMerchantTradeC2s(p) => { - events.3.select_merchant_trade.send(SelectMerchantTrade { - client: entity, - slot: p.selected_slot.0, - }); - } - C2sPlayPacket::UpdateBeaconC2s(p) => { - events.3.update_beacon.send(UpdateBeacon { - client: entity, - primary_effect: p.primary_effect.map(|i| i.0), - secondary_effect: p.secondary_effect.map(|i| i.0), - }); - } - C2sPlayPacket::UpdateSelectedSlotC2s(p) => { - events.3.update_selected_slot.send(UpdateSelectedSlot { - client: entity, - slot: p.slot, - }) - } - C2sPlayPacket::UpdateCommandBlockC2s(p) => { - events.3.update_command_block.send(UpdateCommandBlock { - client: entity, - position: p.position, - command: p.command.into(), - mode: p.mode, - track_output: p.flags.track_output(), - conditional: p.flags.conditional(), - automatic: p.flags.automatic(), - }); - } - C2sPlayPacket::UpdateCommandBlockMinecartC2s(p) => { - events - .3 - .update_command_block_minecart - .send(UpdateCommandBlockMinecart { - client: entity, - entity_id: p.entity_id.0, - command: p.command.into(), - track_output: p.track_output, - }); - } - C2sPlayPacket::CreativeInventoryActionC2s(p) => { - if p.slot == -1 { - if let Some(stack) = p.clicked_item.as_ref() { - events.2.drop_item_stack.send(DropItemStack { - client: entity, - from_slot: None, - stack: stack.clone(), - }); - } - } - events - .3 - .creative_inventory_action - .send(CreativeInventoryAction { - client: entity, - slot: p.slot, - clicked_item: p.clicked_item, - }); - } - C2sPlayPacket::UpdateJigsawC2s(p) => { - events.4.update_jigsaw.send(UpdateJigsaw { - client: entity, - position: p.position, - name: p.name.into(), - target: p.target.into(), - pool: p.pool.into(), - final_state: p.final_state.into(), - joint_type: p.joint_type.into(), - }); - } - C2sPlayPacket::UpdateStructureBlockC2s(p) => { - events.4.update_structure_block.send(UpdateStructureBlock { - client: entity, - position: p.position, - action: p.action, - mode: p.mode, - name: p.name.into(), - offset_xyz: p.offset_xyz, - size_xyz: p.size_xyz, - mirror: p.mirror, - rotation: p.rotation, - metadata: p.metadata.into(), - integrity: p.integrity, - seed: p.seed.0, - flags: p.flags, - }) - } - C2sPlayPacket::UpdateSignC2s(p) => { - events.4.update_sign.send(UpdateSign { - client: entity, - position: p.position, - lines: p.lines.map(Into::into), - }); - } - C2sPlayPacket::HandSwingC2s(p) => { - events.4.hand_swing.send(HandSwing { - client: entity, - hand: p.hand, - }); - } - C2sPlayPacket::SpectatorTeleportC2s(p) => { - events.4.spectator_teleport.send(SpectatorTeleport { - client: entity, - target: p.target, - }); - } - C2sPlayPacket::PlayerInteractBlockC2s(p) => { - if p.sequence.0 != 0 { - q.player_action_sequence.0 = cmp::max(p.sequence.0, q.player_action_sequence.0); - } - - events.4.player_interact_block.send(PlayerInteractBlock { - client: entity, - hand: p.hand, - position: p.position, - direction: p.face, - cursor_pos: p.cursor_pos.into(), - head_inside_block: false, - sequence: 0, - }) - } - C2sPlayPacket::PlayerInteractItemC2s(p) => { - if p.sequence.0 != 0 { - q.player_action_sequence.0 = cmp::max(p.sequence.0, q.player_action_sequence.0); - } - - events.4.player_interact_item.send(PlayerInteractItem { - client: entity, - hand: p.hand, - sequence: p.sequence.0, - }); - } - } - - Ok(true) -} diff --git a/crates/valence/src/client/interact_entity.rs b/crates/valence/src/client/interact_entity.rs new file mode 100644 index 0000000..38cc428 --- /dev/null +++ b/crates/valence/src/client/interact_entity.rs @@ -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::().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, + entities: Res, + mut events: EventWriter, +) { + for packet in packets.iter() { + if let Some(pkt) = packet.decode::() { + // 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, + }) + } + } + } +} diff --git a/crates/valence/src/client/keepalive.rs b/crates/valence/src/client/keepalive.rs new file mode 100644 index 0000000..e78e13a --- /dev/null +++ b/crates/valence/src/client/keepalive.rs @@ -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, + 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::(); + } + } + } +} + +fn handle_keepalive_response( + mut packets: EventReader, + mut clients: Query<(Entity, &mut KeepaliveState, &mut Ping)>, + mut commands: Commands, +) { + for packet in packets.iter() { + if let Some(pkt) = packet.decode::() { + 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::(); + } 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::(); + } else { + state.got_keepalive = true; + ping.0 = state.keepalive_sent_time.elapsed().as_millis() as i32; + } + } + } + } +} diff --git a/crates/valence/src/client/misc.rs b/crates/valence/src/client/misc.rs new file mode 100644 index 0000000..cda4b65 --- /dev/null +++ b/crates/valence/src/client/misc.rs @@ -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::() + .add_event::() + .add_event::() + .add_event::() + .add_event::() + .add_event::() + .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, + 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, + mut clients: Query<&mut ActionSequence>, + mut hand_swing_events: EventWriter, + mut interact_block_events: EventWriter, + mut chat_message_events: EventWriter, + mut respawn_events: EventWriter, + mut request_stats_events: EventWriter, + mut resource_pack_status_change_events: EventWriter, +) { + for packet in packets.iter() { + if let Some(pkt) = packet.decode::() { + hand_swing_events.send(HandSwing { + client: packet.client, + hand: pkt.hand, + }); + } else if let Some(pkt) = packet.decode::() { + 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::() { + 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::() { + chat_message_events.send(ChatMessage { + client: packet.client, + message: pkt.message.into(), + timestamp: pkt.timestamp, + }); + } else if let Some(pkt) = packet.decode::() { + 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::() { + 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, + }, + }); + } + } +} diff --git a/crates/valence/src/client/movement.rs b/crates/valence/src/client/movement.rs new file mode 100644 index 0000000..16e01c3 --- /dev/null +++ b/crates/valence/src/client/movement.rs @@ -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::() + .add_event::() + .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, + mut clients: Query<( + &mut Position, + &mut Look, + &mut HeadYaw, + &mut OnGround, + &mut TeleportState, + )>, + mut movement_events: EventWriter, +) { + for packet in packets.iter() { + if let Some(pkt) = packet.decode::() { + 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::() { + 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::() { + 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::() { + 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::() { + 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, + mut look: Mut, + mut head_yaw: Mut, + mut on_ground: Mut, + mut teleport_state: Mut, + movement_events: &mut EventWriter, +) { + 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); +} diff --git a/crates/valence/src/client/settings.rs b/crates/valence/src/client/settings.rs new file mode 100644 index 0000000..e780b6f --- /dev/null +++ b/crates/valence/src/client/settings.rs @@ -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, + 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, + mut clients: Query<( + &mut ViewDistance, + &mut ClientSettings, + &mut PlayerModelParts, + &mut MainArm, + )>, +) { + for packet in packets.iter() { + if let Some(pkt) = packet.decode::() { + 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)); + } + } + } +} diff --git a/crates/valence/src/client/teleport.rs b/crates/valence/src/client/teleport.rs new file mode 100644 index 0000000..c1bd436 --- /dev/null +++ b/crates/valence/src/client/teleport.rs @@ -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, Changed)>, + >, +) { + 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, + mut clients: Query<&mut TeleportState>, + mut commands: Commands, +) { + for packet in packets.iter() { + if let Some(pkt) = packet.decode::() { + 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::(); + } + + 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::(); + } + } + } + } +} diff --git a/crates/valence/src/component.rs b/crates/valence/src/component.rs index 2d0bb1b..6c3411e 100644 --- a/crates/valence/src/component.rs +++ b/crates/valence/src/component.rs @@ -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; diff --git a/crates/valence/src/entity.rs b/crates/valence/src/entity.rs index 5781871..2078326 100644 --- a/crates/valence/src/entity.rs +++ b/crates/valence/src/entity.rs @@ -437,12 +437,12 @@ impl EntityManager { } /// Gets the entity with the given entity ID. - pub fn get_with_id(&self, entity_id: i32) -> Option { + pub fn get_by_id(&self, entity_id: i32) -> Option { self.id_to_entity.get(&entity_id).cloned() } /// Gets the entity with the given UUID. - pub fn get_with_uuid(&self, uuid: Uuid) -> Option { + pub fn get_by_uuid(&self, uuid: Uuid) -> Option { self.uuid_to_entity.get(&uuid).cloned() } } diff --git a/crates/valence/src/event_loop.rs b/crates/valence/src/event_loop.rs new file mode 100644 index 0000000..fed9622 --- /dev/null +++ b/crates/valence/src/event_loop.rs @@ -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::(); + + // 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

+ 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, + Commands, + )>, + mut check_again: Local>, +) { + 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::(); + } + } + } + + 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::(); + false + } + } + } else { + // Client must have been deleted in the last run of the schedule. + false + } + }); + + state.apply(world); + world.run_schedule(EventLoopSchedule); + } +} diff --git a/crates/valence/src/inventory.rs b/crates/valence/src/inventory.rs index 255ba95..f5c65dd 100644 --- a/crates/valence/src/inventory.rs +++ b/crates/valence/src/inventory.rs @@ -30,10 +30,15 @@ use std::borrow::Cow; use std::iter::FusedIterator; use std::ops::Range; -use bevy_app::{CoreSet, Plugin}; +use bevy_app::prelude::*; use bevy_ecs::prelude::*; use tracing::{debug, warn}; use valence_protocol::item::ItemStack; +use valence_protocol::packet::c2s::play::click_slot::{ClickMode, Slot}; +use valence_protocol::packet::c2s::play::{ + ClickSlotC2s, CloseHandledScreenC2s, CreativeInventoryActionC2s, PlayerActionC2s, + UpdateSelectedSlotC2s, +}; use valence_protocol::packet::s2c::play::{ CloseScreenS2c, InventoryS2c, OpenScreenS2c, ScreenHandlerSlotUpdateS2c, }; @@ -41,17 +46,44 @@ use valence_protocol::text::Text; use valence_protocol::types::WindowType; use valence_protocol::var_int::VarInt; -use crate::client::event::{ - ClickSlot, CloseHandledScreen, CreativeInventoryAction, UpdateSelectedSlot, -}; -use crate::client::{Client, CursorItem, PlayerInventoryState}; +use crate::client::{Client, ClientInventoryState, CursorItem, FlushPacketsSet}; use crate::component::GameMode; +use crate::event_loop::{EventLoopSchedule, EventLoopSet, PacketEvent}; use crate::packet::WritePacket; -use crate::prelude::FlushPacketsSet; mod validate; -pub(crate) use validate::*; +pub(crate) struct InventoryPlugin; + +impl Plugin for InventoryPlugin { + fn build(&self, app: &mut bevy_app::App) { + app.add_systems( + ( + update_open_inventories, + update_client_on_close_inventory.after(update_open_inventories), + update_player_inventories, + ) + .in_base_set(CoreSet::PostUpdate) + .before(FlushPacketsSet), + ) + .add_systems( + ( + handle_update_selected_slot, + handle_click_slot, + handle_creative_inventory_action, + handle_close_handled_screen, + handle_player_actions, + ) + .in_base_set(EventLoopSet::PreUpdate) + .in_schedule(EventLoopSchedule), + ) + .init_resource::() + .add_event::() + .add_event::() + .add_event::() + .add_event::(); + } +} /// The number of slots in the "main" part of the player inventory. 3 rows of 9, /// plus the hotbar. @@ -292,9 +324,9 @@ impl Inventory { /// an inventory. #[derive(Component, Clone, Debug)] pub struct OpenInventory { - /// The Entity with the `Inventory` component that the client is currently + /// The entity with the `Inventory` component that the client is currently /// viewing. - pub(crate) entity: Entity, + pub entity: Entity, client_changed: u64, } @@ -305,10 +337,6 @@ impl OpenInventory { client_changed: 0, } } - - pub fn entity(&self) -> Entity { - self.entity - } } /// A helper to represent the inventory window that the player is currently @@ -446,37 +474,13 @@ impl<'a> InventoryWindowMut<'a> { } } -pub(crate) struct InventoryPlugin; - -impl Plugin for InventoryPlugin { - fn build(&self, app: &mut bevy_app::App) { - app.add_systems( - ( - handle_set_held_item, - handle_click_container - .before(update_open_inventories) - .before(update_player_inventories), - handle_set_slot_creative - .before(update_open_inventories) - .before(update_player_inventories), - update_open_inventories, - handle_close_container, - update_client_on_close_inventory.after(update_open_inventories), - update_player_inventories, - ) - .in_base_set(CoreSet::PostUpdate) - .before(FlushPacketsSet), - ); - } -} - /// Send updates for each client's player inventory. fn update_player_inventories( mut query: Query< ( &mut Inventory, &mut Client, - &mut PlayerInventoryState, + &mut ClientInventoryState, Ref, ), Without, @@ -553,7 +557,7 @@ fn update_open_inventories( mut clients: Query<( Entity, &mut Client, - &mut PlayerInventoryState, + &mut ClientInventoryState, &CursorItem, &mut OpenInventory, )>, @@ -611,7 +615,7 @@ fn update_open_inventories( } else { // Send the changed slots. - // The slots that were NOT changed by this client, and they need to be sent + // The slots that were NOT changed by this client, and they need to be sent. let changed_filtered = inventory.changed & !open_inventory.client_changed; if changed_filtered != 0 { @@ -639,10 +643,12 @@ fn update_open_inventories( } /// Handles clients telling the server that they are closing an inventory. -fn handle_close_container(mut events: EventReader, mut commands: Commands) { - for event in events.iter() { - if let Some(mut entity) = commands.get_entity(event.client) { - entity.remove::(); +fn handle_close_handled_screen(mut packets: EventReader, mut commands: Commands) { + for packet in packets.iter() { + if packet.decode::().is_some() { + if let Some(mut entity) = commands.get_entity(packet.client) { + entity.remove::(); + } } } } @@ -651,7 +657,7 @@ fn handle_close_container(mut events: EventReader, mut comma /// indicates that the client is no longer viewing an inventory. fn update_client_on_close_inventory( mut removals: RemovedComponents, - mut clients: Query<(&mut Client, &PlayerInventoryState)>, + mut clients: Query<(&mut Client, &ClientInventoryState)>, ) { for entity in &mut removals { if let Ok((mut client, inv_state)) = clients.get_mut(entity) { @@ -662,142 +668,332 @@ fn update_client_on_close_inventory( } } -// TODO: Do this logic in c2s packet handler? -fn handle_click_container( +// TODO: make this event user friendly. +#[derive(Clone, Debug)] +pub struct ClickSlot { + pub client: Entity, + pub window_id: u8, + pub state_id: i32, + pub slot_id: i16, + pub button: i8, + pub mode: ClickMode, + pub slot_changes: Vec, + pub carried_item: Option, +} + +#[derive(Clone, Debug)] +pub struct DropItemStack { + pub client: Entity, + pub from_slot: Option, + pub stack: ItemStack, +} + +fn handle_click_slot( + mut packets: EventReader, mut clients: Query<( &mut Client, &mut Inventory, - &mut PlayerInventoryState, + &mut ClientInventoryState, Option<&mut OpenInventory>, &mut CursorItem, )>, - // TODO: this query matches disconnected clients. Define client marker component to avoid - // problem? mut inventories: Query<&mut Inventory, Without>, - mut events: EventReader, + mut drop_item_stack_events: EventWriter, + mut click_slot_events: EventWriter, ) { - for event in events.iter() { - let Ok((mut client, mut client_inventory, mut inv_state, open_inventory, mut cursor_item)) = - clients.get_mut(event.client) else { - // The client does not exist, ignore. - continue; - }; + for packet in packets.iter() { + let Some(pkt) = packet.decode::() else { + // Not the packet we're looking for. + continue + }; - // Validate the window id. - if (event.window_id == 0) != open_inventory.is_none() { - warn!( - "Client sent a click with an invalid window id for current state: window_id = {}, \ - open_inventory present = {}", - event.window_id, - open_inventory.is_some() + let Ok(( + mut client, + mut client_inv, + mut inv_state, + open_inventory, + mut cursor_item + )) = clients.get_mut(packet.client) else { + // The client does not exist, ignore. + continue; + }; + + let open_inv = open_inventory + .as_ref() + .and_then(|open| inventories.get_mut(open.entity).ok()); + + if let Err(e) = validate::validate_click_slot_packet( + &pkt, + &client_inv, + open_inv.as_deref(), + &cursor_item, + ) { + debug!( + "failed to validate click slot packet for client {:#?}: \"{e:#}\" {pkt:#?}", + packet.client ); + + // Resync the inventory. + + client.write_packet(&InventoryS2c { + window_id: if open_inv.is_some() { + inv_state.window_id + } else { + 0 + }, + state_id: VarInt(inv_state.state_id.0), + slots: Cow::Borrowed(open_inv.unwrap_or(client_inv).slot_slice()), + carried_item: Cow::Borrowed(&cursor_item.0), + }); + continue; } - if let Some(mut open_inventory) = open_inventory { - // The player is interacting with an inventory that is open. + if pkt.slot_idx < 0 && pkt.mode == ClickMode::Click { + // The client is dropping the cursor item by clicking outside the window. - let Ok(mut target_inventory) = inventories.get_mut(open_inventory.entity) else { - // The inventory does not exist, ignore. - continue; - }; - - if inv_state.state_id.0 != event.state_id { - // Client is out of sync. Resync and ignore click. - - debug!("Client state id mismatch, resyncing"); - - inv_state.state_id += 1; - - client.write_packet(&InventoryS2c { - window_id: inv_state.window_id, - state_id: VarInt(inv_state.state_id.0), - slots: Cow::Borrowed(target_inventory.slot_slice()), - carried_item: Cow::Borrowed(&cursor_item.0), + if let Some(stack) = cursor_item.0.take() { + drop_item_stack_events.send(DropItemStack { + client: packet.client, + from_slot: None, + stack, }); - - continue; } + } else if pkt.mode == ClickMode::DropKey { + // The client is dropping an item by pressing the drop key. - cursor_item.set_if_neq(CursorItem(event.carried_item.clone())); - - for slot in event.slot_changes.clone() { - if (0i16..target_inventory.slot_count() as i16).contains(&slot.idx) { - // The client is interacting with a slot in the target inventory. - target_inventory.set_slot(slot.idx as u16, slot.item); - open_inventory.client_changed |= 1 << slot.idx; + let entire_stack = pkt.button == 1; + if let Some(stack) = client_inv.slot(pkt.slot_idx as u16) { + // TODO: is the use of `replace_slot` here causing unnecessary packets to be + // sent? + let dropped = if entire_stack || stack.count() == 1 { + client_inv.replace_slot(pkt.slot_idx as u16, None) } else { - // The client is interacting with a slot in their own inventory. - let slot_id = convert_to_player_slot_id(target_inventory.kind, slot.idx as u16); - client_inventory.set_slot(slot_id, slot.item); - inv_state.slots_changed |= 1 << slot_id; + let mut stack = stack.clone(); + stack.set_count(stack.count() - 1); + let mut old_slot = client_inv.replace_slot(pkt.slot_idx as u16, Some(stack)); + // we already checked that the slot was not empty and that the + // stack count is > 1 + old_slot.as_mut().unwrap().set_count(1); + old_slot } + .expect("dropped item should exist"); // we already checked that the slot was not empty + + drop_item_stack_events.send(DropItemStack { + client: packet.client, + from_slot: Some(pkt.slot_idx as u16), + stack: dropped, + }); } } else { - // The client is interacting with their own inventory. - - if inv_state.state_id.0 != event.state_id { - // Client is out of sync. Resync and ignore the click. - - debug!("Client state id mismatch, resyncing"); - - inv_state.state_id += 1; - - client.write_packet(&InventoryS2c { - window_id: inv_state.window_id, - state_id: VarInt(inv_state.state_id.0), - slots: Cow::Borrowed(client_inventory.slot_slice()), - carried_item: Cow::Borrowed(&cursor_item.0), - }); + // The player is clicking a slot in an inventory. + // Validate the window id. + if (pkt.window_id == 0) != open_inventory.is_none() { + warn!( + "Client sent a click with an invalid window id for current state: window_id = \ + {}, open_inventory present = {}", + pkt.window_id, + open_inventory.is_some() + ); continue; } - cursor_item.set_if_neq(CursorItem(event.carried_item.clone())); - inv_state.client_updated_cursor_item = true; + if let Some(mut open_inventory) = open_inventory { + // The player is interacting with an inventory that is open. - for slot in event.slot_changes.clone() { - if (0i16..client_inventory.slot_count() as i16).contains(&slot.idx) { - client_inventory.set_slot(slot.idx as u16, slot.item); - inv_state.slots_changed |= 1 << slot.idx; - } else { - // The client is trying to interact with a slot that does not exist, - // ignore. - warn!( - "Client attempted to interact with slot {} which does not exist", - slot.idx - ); + let Ok(mut target_inventory) = inventories.get_mut(open_inventory.entity) else { + // The inventory does not exist, ignore. + continue; + }; + + if inv_state.state_id.0 != pkt.state_id.0 { + // Client is out of sync. Resync and ignore click. + + debug!("Client state id mismatch, resyncing"); + + inv_state.state_id += 1; + + client.write_packet(&InventoryS2c { + window_id: inv_state.window_id, + state_id: VarInt(inv_state.state_id.0), + slots: Cow::Borrowed(target_inventory.slot_slice()), + carried_item: Cow::Borrowed(&cursor_item.0), + }); + + continue; } + + cursor_item.set_if_neq(CursorItem(pkt.carried_item.clone())); + + for slot in pkt.slot_changes.clone() { + if (0i16..target_inventory.slot_count() as i16).contains(&slot.idx) { + // The client is interacting with a slot in the target inventory. + target_inventory.set_slot(slot.idx as u16, slot.item); + open_inventory.client_changed |= 1 << slot.idx; + } else { + // The client is interacting with a slot in their own inventory. + let slot_id = + convert_to_player_slot_id(target_inventory.kind, slot.idx as u16); + client_inv.set_slot(slot_id, slot.item); + inv_state.slots_changed |= 1 << slot_id; + } + } + } else { + // The client is interacting with their own inventory. + + if inv_state.state_id.0 != pkt.state_id.0 { + // Client is out of sync. Resync and ignore the click. + + debug!("Client state id mismatch, resyncing"); + + inv_state.state_id += 1; + + client.write_packet(&InventoryS2c { + window_id: inv_state.window_id, + state_id: VarInt(inv_state.state_id.0), + slots: Cow::Borrowed(client_inv.slot_slice()), + carried_item: Cow::Borrowed(&cursor_item.0), + }); + + continue; + } + + cursor_item.set_if_neq(CursorItem(pkt.carried_item.clone())); + inv_state.client_updated_cursor_item = true; + + for slot in pkt.slot_changes.clone() { + if (0i16..client_inv.slot_count() as i16).contains(&slot.idx) { + client_inv.set_slot(slot.idx as u16, slot.item); + inv_state.slots_changed |= 1 << slot.idx; + } else { + // The client is trying to interact with a slot that does not exist, + // ignore. + warn!( + "Client attempted to interact with slot {} which does not exist", + slot.idx + ); + } + } + } + + click_slot_events.send(ClickSlot { + client: packet.client, + window_id: pkt.window_id, + state_id: pkt.state_id.0, + slot_id: pkt.slot_idx, + button: pkt.button, + mode: pkt.mode, + slot_changes: pkt.slot_changes, + carried_item: pkt.carried_item, + }); + } + } +} + +fn handle_player_actions( + mut packets: EventReader, + mut clients: Query<(&mut Inventory, &mut ClientInventoryState)>, + mut drop_item_stack_events: EventWriter, +) { + for packet in packets.iter() { + if let Some(pkt) = packet.decode::() { + use valence_protocol::packet::c2s::play::player_action::Action; + + match pkt.action { + Action::DropAllItems => { + if let Ok((mut inv, mut inv_state)) = clients.get_mut(packet.client) { + if let Some(stack) = inv.replace_slot(inv_state.held_item_slot, None) { + inv_state.slots_changed |= 1 << inv_state.held_item_slot; + + drop_item_stack_events.send(DropItemStack { + client: packet.client, + from_slot: Some(inv_state.held_item_slot), + stack, + }); + } + } + } + Action::DropItem => { + if let Ok((mut inv, mut inv_state)) = clients.get_mut(packet.client) { + if let Some(mut stack) = inv.replace_slot(inv_state.held_item_slot(), None) + { + if stack.count() > 1 { + inv.set_slot( + inv_state.held_item_slot(), + stack.clone().with_count(stack.count() - 1), + ); + + stack.set_count(1); + } + + inv_state.slots_changed |= 1 << inv_state.held_item_slot(); + + drop_item_stack_events.send(DropItemStack { + client: packet.client, + from_slot: Some(inv_state.held_item_slot()), + stack, + }) + } + } + } + Action::SwapItemWithOffhand => { + // TODO + } + _ => {} } } } } -fn handle_set_slot_creative( +// TODO: make this event user friendly. +#[derive(Clone, Debug)] +pub struct CreativeInventoryAction { + pub client: Entity, + pub slot: i16, + pub clicked_item: Option, +} + +fn handle_creative_inventory_action( + mut packets: EventReader, mut clients: Query<( &mut Client, &mut Inventory, - &mut PlayerInventoryState, + &mut ClientInventoryState, &GameMode, )>, - mut events: EventReader, + mut inv_action_events: EventWriter, + mut drop_item_stack_events: EventWriter, ) { - for event in events.iter() { - if let Ok((mut client, mut inventory, mut inv_state, game_mode)) = - clients.get_mut(event.client) - { + for packet in packets.iter() { + if let Some(pkt) = packet.decode::() { + let Ok((mut client, mut inventory, mut inv_state, game_mode)) = clients.get_mut(packet.client) else { + continue + }; + if *game_mode != GameMode::Creative { // The client is not in creative mode, ignore. continue; } - if event.slot < 0 || event.slot >= inventory.slot_count() as i16 { + if pkt.slot == -1 { + if let Some(stack) = pkt.clicked_item.clone() { + drop_item_stack_events.send(DropItemStack { + client: packet.client, + from_slot: None, + stack, + }); + } + continue; + } + + if pkt.slot < 0 || pkt.slot >= inventory.slot_count() as i16 { // The client is trying to interact with a slot that does not exist, ignore. continue; } // Set the slot without marking it as changed. - inventory.slots[event.slot as usize] = event.clicked_item.clone(); + inventory.slots[pkt.slot as usize] = pkt.clicked_item.clone(); inv_state.state_id += 1; @@ -808,20 +1004,41 @@ fn handle_set_slot_creative( client.write_packet(&ScreenHandlerSlotUpdateS2c { window_id: 0, state_id: VarInt(inv_state.state_id.0), - slot_idx: event.slot, - slot_data: Cow::Borrowed(&event.clicked_item), + slot_idx: pkt.slot, + slot_data: Cow::Borrowed(&pkt.clicked_item), + }); + + inv_action_events.send(CreativeInventoryAction { + client: packet.client, + slot: pkt.slot, + clicked_item: pkt.clicked_item, }); } } } -fn handle_set_held_item( - mut clients: Query<&mut PlayerInventoryState>, - mut events: EventReader, +#[derive(Clone, Debug)] +pub struct UpdateSelectedSlot { + pub client: Entity, + pub slot: i16, +} + +fn handle_update_selected_slot( + mut packets: EventReader, + mut clients: Query<&mut ClientInventoryState>, + mut events: EventWriter, ) { - for event in events.iter() { - if let Ok(mut inv_state) = clients.get_mut(event.client) { - inv_state.held_item_slot = convert_hotbar_slot_id(event.slot as u16); + for packet in packets.iter() { + if let Some(pkt) = packet.decode::() { + if let Ok(mut inv_state) = clients.get_mut(packet.client) { + // TODO: validate this. + inv_state.held_item_slot = convert_hotbar_slot_id(pkt.slot as u16); + + events.send(UpdateSelectedSlot { + client: packet.client, + slot: pkt.slot, + }); + } } } } @@ -968,13 +1185,13 @@ impl From for InventoryKind { #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Resource)] pub struct InventorySettings { - pub enable_item_dupe_check: bool, + pub validate_actions: bool, } impl Default for InventorySettings { fn default() -> Self { Self { - enable_item_dupe_check: true, + validate_actions: true, } } } @@ -1007,7 +1224,7 @@ mod test { } #[test] - fn test_should_open_inventory() -> anyhow::Result<()> { + fn test_should_open_inventory() { let mut app = App::new(); let (client_ent, mut client_helper) = scenario_single_client(&mut app); @@ -1028,7 +1245,7 @@ mod test { app.update(); // Make assertions - 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(_)); @@ -1037,12 +1254,10 @@ mod test { S2cPlayPacket::OpenScreenS2c(_), S2cPlayPacket::InventoryS2c(_) ); - - Ok(()) } #[test] - fn test_should_close_inventory() -> anyhow::Result<()> { + fn test_should_close_inventory() { let mut app = App::new(); let (client_ent, mut client_helper) = scenario_single_client(&mut app); @@ -1072,15 +1287,13 @@ mod test { app.update(); // Make assertions - let sent_packets = client_helper.collect_sent()?; + let sent_packets = client_helper.collect_sent(); assert_packet_count!(sent_packets, 1, S2cPlayPacket::CloseScreenS2c(_)); - - Ok(()) } #[test] - fn test_should_remove_invalid_open_inventory() -> anyhow::Result<()> { + fn test_should_remove_invalid_open_inventory() { let mut app = App::new(); let (client_ent, mut client_helper) = scenario_single_client(&mut app); @@ -1108,14 +1321,12 @@ mod test { // Make assertions assert!(app.world.get::(client_ent).is_none()); - let sent_packets = client_helper.collect_sent()?; + let sent_packets = client_helper.collect_sent(); assert_packet_count!(sent_packets, 1, S2cPlayPacket::CloseScreenS2c(_)); - - Ok(()) } #[test] - fn test_should_modify_player_inventory_click_slot() -> anyhow::Result<()> { + fn test_should_modify_player_inventory_click_slot() { let mut app = App::new(); let (client_ent, mut client_helper) = scenario_single_client(&mut app); let mut inventory = app @@ -1131,7 +1342,7 @@ mod test { // Make the client click the slot and pick up the item. let state_id = app .world - .get::(client_ent) + .get::(client_ent) .unwrap() .state_id; client_helper.send(&valence_protocol::packet::c2s::play::ClickSlotC2s { @@ -1140,7 +1351,7 @@ mod test { mode: valence_protocol::packet::c2s::play::click_slot::ClickMode::Click, state_id: VarInt(state_id.0), slot_idx: 20, - slots: vec![valence_protocol::packet::c2s::play::click_slot::Slot { + slot_changes: vec![valence_protocol::packet::c2s::play::click_slot::Slot { idx: 20, item: None, }], @@ -1150,7 +1361,7 @@ mod test { app.update(); // Make assertions - let sent_packets = client_helper.collect_sent()?; + let sent_packets = client_helper.collect_sent(); // because the inventory was changed as a result of the client's click, the // server should not send any packets to the client because the client @@ -1173,12 +1384,10 @@ mod test { cursor_item.0, Some(ItemStack::new(ItemKind::Diamond, 2, None)) ); - - Ok(()) } #[test] - fn test_should_modify_player_inventory_server_side() -> anyhow::Result<()> { + fn test_should_modify_player_inventory_server_side() { let mut app = App::new(); let (client_ent, mut client_helper) = scenario_single_client(&mut app); let mut inventory = app @@ -1201,7 +1410,7 @@ mod test { app.update(); // Make assertions - let sent_packets = client_helper.collect_sent()?; + let sent_packets = client_helper.collect_sent(); // because the inventory was modified server side, the client needs to be // updated with the change. assert_packet_count!( @@ -1209,12 +1418,10 @@ mod test { 1, S2cPlayPacket::ScreenHandlerSlotUpdateS2c(_) ); - - Ok(()) } #[test] - fn test_should_sync_entire_player_inventory() -> anyhow::Result<()> { + fn test_should_sync_entire_player_inventory() { let mut app = App::new(); let (client_ent, mut client_helper) = scenario_single_client(&mut app); @@ -1231,10 +1438,8 @@ mod test { app.update(); // Make assertions - let sent_packets = client_helper.collect_sent()?; + let sent_packets = client_helper.collect_sent(); assert_packet_count!(sent_packets, 1, S2cPlayPacket::InventoryS2c(_)); - - Ok(()) } fn set_up_open_inventory(app: &mut App, client_ent: Entity) -> Entity { @@ -1252,7 +1457,7 @@ mod test { } #[test] - fn test_should_modify_open_inventory_click_slot() -> anyhow::Result<()> { + fn test_should_modify_open_inventory_click_slot() { let mut app = App::new(); let (client_ent, mut client_helper) = scenario_single_client(&mut app); let inventory_ent = set_up_open_inventory(&mut app, client_ent); @@ -1267,7 +1472,7 @@ mod test { client_helper.clear_sent(); // Make the client click the slot and pick up the item. - let inv_state = app.world.get::(client_ent).unwrap(); + let inv_state = app.world.get::(client_ent).unwrap(); let state_id = inv_state.state_id; let window_id = inv_state.window_id; client_helper.send(&valence_protocol::packet::c2s::play::ClickSlotC2s { @@ -1276,7 +1481,7 @@ mod test { mode: valence_protocol::packet::c2s::play::click_slot::ClickMode::Click, state_id: VarInt(state_id.0), slot_idx: 20, - slots: vec![valence_protocol::packet::c2s::play::click_slot::Slot { + slot_changes: vec![valence_protocol::packet::c2s::play::click_slot::Slot { idx: 20, item: None, }], @@ -1286,7 +1491,7 @@ mod test { app.update(); // Make assertions - let sent_packets = client_helper.collect_sent()?; + let sent_packets = client_helper.collect_sent(); // because the inventory was modified as a result of the client's click, the // server should not send any packets to the client because the client @@ -1309,12 +1514,10 @@ mod test { cursor_item.0, Some(ItemStack::new(ItemKind::Diamond, 2, None)) ); - - Ok(()) } #[test] - fn test_should_modify_open_inventory_server_side() -> anyhow::Result<()> { + fn test_should_modify_open_inventory_server_side() { let mut app = App::new(); let (client_ent, mut client_helper) = scenario_single_client(&mut app); let inventory_ent = set_up_open_inventory(&mut app, client_ent); @@ -1333,7 +1536,7 @@ mod test { app.update(); // Make assertions - let sent_packets = client_helper.collect_sent()?; + let sent_packets = client_helper.collect_sent(); // because the inventory was modified server side, the client needs to be // updated with the change. @@ -1350,12 +1553,10 @@ mod test { inventory.slot(5), Some(&ItemStack::new(ItemKind::IronIngot, 1, None)) ); - - Ok(()) } #[test] - fn test_should_sync_entire_open_inventory() -> anyhow::Result<()> { + fn test_should_sync_entire_open_inventory() { let mut app = App::new(); let (client_ent, mut client_helper) = scenario_single_client(&mut app); let inventory_ent = set_up_open_inventory(&mut app, client_ent); @@ -1373,10 +1574,8 @@ mod test { app.update(); // Make assertions - let sent_packets = client_helper.collect_sent()?; + let sent_packets = client_helper.collect_sent(); assert_packet_count!(sent_packets, 1, S2cPlayPacket::InventoryS2c(_)); - - Ok(()) } #[test] @@ -1475,13 +1674,13 @@ mod test { // Make assertions let inv_state = app .world - .get::(client_ent) + .get::(client_ent) .expect("could not find client"); assert_eq!(inv_state.window_id, 3); } #[test] - fn test_should_handle_set_held_item() -> anyhow::Result<()> { + fn test_should_handle_set_held_item() { let mut app = App::new(); let (client_ent, mut client_helper) = scenario_single_client(&mut app); @@ -1496,21 +1695,19 @@ mod test { // Make assertions let inv_state = app .world - .get::(client_ent) + .get::(client_ent) .expect("could not find client"); assert_eq!(inv_state.held_item_slot, 40); - - Ok(()) } #[test] - fn should_not_increment_state_id_on_cursor_item_change() -> anyhow::Result<()> { + fn should_not_increment_state_id_on_cursor_item_change() { let mut app = App::new(); let (client_ent, mut client_helper) = scenario_single_client(&mut app); let inv_state = app .world - .get::(client_ent) + .get::(client_ent) .expect("could not find client"); let expected_state_id = inv_state.state_id.0; @@ -1526,14 +1723,12 @@ mod test { // Make assertions let inv_state = app .world - .get::(client_ent) + .get::(client_ent) .expect("could not find client"); assert_eq!( inv_state.state_id.0, expected_state_id, "state id should not have changed" ); - - Ok(()) } mod dropping_items { @@ -1543,10 +1738,9 @@ mod test { use valence_protocol::types::Direction; use super::*; - use crate::client::event::DropItemStack; #[test] - fn should_drop_item_player_action() -> anyhow::Result<()> { + fn should_drop_item_player_action() { let mut app = App::new(); let (client_ent, mut client_helper) = scenario_single_client(&mut app); let mut inventory = app @@ -1590,18 +1784,16 @@ mod test { ItemStack::new(ItemKind::IronIngot, 1, None) ); - let sent_packets = client_helper.collect_sent()?; + let sent_packets = client_helper.collect_sent(); assert_packet_count!( sent_packets, 0, S2cPlayPacket::ScreenHandlerSlotUpdateS2c(_) ); - - Ok(()) } #[test] - fn should_drop_item_stack_player_action() -> anyhow::Result<()> { + fn should_drop_item_stack_player_action() { let mut app = App::new(); let (client_ent, mut client_helper) = scenario_single_client(&mut app); let mut inventory = app @@ -1626,7 +1818,7 @@ mod test { // Make assertions let inv_state = app .world - .get::(client_ent) + .get::(client_ent) .expect("could not find client"); assert_eq!(inv_state.held_item_slot, 36); let inventory = app @@ -1646,12 +1838,10 @@ mod test { events[0].stack, ItemStack::new(ItemKind::IronIngot, 32, None) ); - - Ok(()) } #[test] - fn should_drop_item_stack_set_creative_mode_slot() -> anyhow::Result<()> { + fn should_drop_item_stack_set_creative_mode_slot() { let mut app = App::new(); let (client_ent, mut client_helper) = scenario_single_client(&mut app); @@ -1659,6 +1849,8 @@ mod test { app.update(); client_helper.clear_sent(); + app.world.entity_mut(client_ent).insert(GameMode::Creative); + client_helper.send( &valence_protocol::packet::c2s::play::CreativeInventoryActionC2s { slot: -1, @@ -1672,8 +1864,10 @@ mod test { let events = app .world .get_resource::>() - .expect("expected drop item stack events"); - let events = events.iter_current_update_events().collect::>(); + .expect("expected drop item stack events") + .iter_current_update_events() + .collect::>(); + assert_eq!(events.len(), 1); assert_eq!(events[0].client, client_ent); assert_eq!(events[0].from_slot, None); @@ -1681,12 +1875,10 @@ mod test { events[0].stack, ItemStack::new(ItemKind::IronIngot, 32, None) ); - - Ok(()) } #[test] - fn should_drop_item_stack_click_container_outside() -> anyhow::Result<()> { + fn should_drop_item_stack_click_container_outside() { let mut app = App::new(); let (client_ent, mut client_helper) = scenario_single_client(&mut app); let mut cursor_item = app @@ -1696,7 +1888,7 @@ mod test { cursor_item.0 = Some(ItemStack::new(ItemKind::IronIngot, 32, None)); let inv_state = app .world - .get_mut::(client_ent) + .get_mut::(client_ent) .expect("could not find client"); let state_id = inv_state.state_id.0; @@ -1710,7 +1902,7 @@ mod test { button: 0, mode: ClickMode::Click, state_id: VarInt(state_id), - slots: vec![], + slot_changes: vec![], carried_item: None, }); @@ -1734,17 +1926,15 @@ mod test { events[0].stack, ItemStack::new(ItemKind::IronIngot, 32, None) ); - - Ok(()) } #[test] - fn should_drop_item_click_container_with_dropkey_single() -> anyhow::Result<()> { + fn should_drop_item_click_container_with_dropkey_single() { let mut app = App::new(); let (client_ent, mut client_helper) = scenario_single_client(&mut app); let inv_state = app .world - .get_mut::(client_ent) + .get_mut::(client_ent) .expect("could not find client"); let state_id = inv_state.state_id.0; let mut inventory = app @@ -1763,7 +1953,7 @@ mod test { button: 0, mode: ClickMode::DropKey, state_id: VarInt(state_id), - slots: vec![Slot { + slot_changes: vec![Slot { idx: 40, item: Some(ItemStack::new(ItemKind::IronIngot, 31, None)), }], @@ -1785,17 +1975,15 @@ mod test { events[0].stack, ItemStack::new(ItemKind::IronIngot, 1, None) ); - - Ok(()) } #[test] - fn should_drop_item_stack_click_container_with_dropkey() -> anyhow::Result<()> { + fn should_drop_item_stack_click_container_with_dropkey() { let mut app = App::new(); let (client_ent, mut client_helper) = scenario_single_client(&mut app); let inv_state = app .world - .get_mut::(client_ent) + .get_mut::(client_ent) .expect("could not find client"); let state_id = inv_state.state_id.0; let mut inventory = app @@ -1814,7 +2002,7 @@ mod test { button: 1, // pressing control mode: ClickMode::DropKey, state_id: VarInt(state_id), - slots: vec![Slot { + slot_changes: vec![Slot { idx: 40, item: None, }], @@ -1836,13 +2024,11 @@ mod test { events[0].stack, ItemStack::new(ItemKind::IronIngot, 32, None) ); - - Ok(()) } } #[test] - fn dragging_items() -> anyhow::Result<()> { + fn dragging_items() { let mut app = App::new(); let (client_ent, mut client_helper) = scenario_single_client(&mut app); app.world.get_mut::(client_ent).unwrap().0 = @@ -1852,7 +2038,7 @@ mod test { app.update(); client_helper.clear_sent(); - let inv_state = app.world.get::(client_ent).unwrap(); + let inv_state = app.world.get::(client_ent).unwrap(); let window_id = inv_state.window_id; let state_id = inv_state.state_id.0; @@ -1862,7 +2048,7 @@ mod test { slot_idx: -999, button: 2, mode: ClickMode::Drag, - slots: vec![ + slot_changes: vec![ Slot { idx: 9, item: Some(ItemStack::new(ItemKind::Diamond, 21, None)), @@ -1881,7 +2067,7 @@ mod test { client_helper.send(&drag_packet); app.update(); - let sent_packets = client_helper.collect_sent()?; + let sent_packets = client_helper.collect_sent(); assert_eq!(sent_packets.len(), 0); let cursor_item = app @@ -1902,7 +2088,5 @@ mod test { Some(&ItemStack::new(ItemKind::Diamond, 21, None)) ); } - - Ok(()) } } diff --git a/crates/valence/src/inventory/validate.rs b/crates/valence/src/inventory/validate.rs index a1ff190..fb3b106 100644 --- a/crates/valence/src/inventory/validate.rs +++ b/crates/valence/src/inventory/validate.rs @@ -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"); } } diff --git a/crates/valence/src/lib.rs b/crates/valence/src/lib.rs index c23271d..8252b14 100644 --- a/crates/valence/src/lib.rs +++ b/crates/valence/src/lib.rs @@ -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::{ diff --git a/crates/valence/src/packet.rs b/crates/valence/src/packet.rs index 5b903ac..6fe2d9d 100644 --- a/crates/valence/src/packet.rs +++ b/crates/valence/src/packet.rs @@ -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. diff --git a/crates/valence/src/player_list.rs b/crates/valence/src/player_list.rs index 8d32c5b..e8ae882 100644 --- a/crates/valence/src/player_list.rs +++ b/crates/valence/src/player_list.rs @@ -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 diff --git a/crates/valence/src/server.rs b/crates/valence/src/server.rs index 3c803fb..61b828f 100644 --- a/crates/valence/src/server.rs +++ b/crates/valence/src/server.rs @@ -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, /// Sender for new clients past the login stage. - new_clients_send: Sender, + new_clients_send: Sender, /// Receiver for new clients past the login stage. - new_clients_recv: Receiver, + new_clients_recv: Receiver, /// A semaphore used to limit the number of simultaneous connections to the /// server. Closing this semaphore stops new connections. connection_sema: Arc, @@ -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) diff --git a/crates/valence/src/server/connect.rs b/crates/valence/src/server/connect.rs index 091be80..ff6cf2a 100644 --- a/crates/valence/src/server/connect.rs +++ b/crates/valence/src/server/connect.rs @@ -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, diff --git a/crates/valence/src/server/connection.rs b/crates/valence/src/server/connection.rs index 966c950..a463be2 100644 --- a/crates/valence/src/server/connection.rs +++ b/crates/valence/src/server/connection.rs @@ -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 { @@ -23,6 +24,7 @@ pub(super) struct InitialConnection { 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) { 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::() + 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, + 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, + /// Limits the amount of data queued in the `recv` channel. Each permit + /// represents one byte. + recv_sem: Arc, + /// 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 { + fn try_recv(&mut self) -> anyhow::Result> { 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::() + 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() + } } diff --git a/crates/valence/src/unit_test/example.rs b/crates/valence/src/unit_test/example.rs index da55c49..98ac16b 100644 --- a/crates/valence/src/unit_test/example.rs +++ b/crates/valence/src/unit_test/example.rs @@ -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_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(()) } } diff --git a/crates/valence/src/unit_test/util.rs b/crates/valence/src/unit_test/util.rs index 6b5b8b1..65eaf56 100644 --- a/crates/valence/src/unit_test/util.rs +++ b/crates/valence/src/unit_test/util.rs @@ -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) -> NewClientInfo { /// Safe to clone, but note that the clone will share the same buffers. #[derive(Clone)] pub(crate) struct MockClientConnection { - buffers: Arc>, + inner: Arc>, } -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, /// 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 { - 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> { + 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, } 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>> { + pub fn collect_sent(&mut self) -> Vec { self.dec.queue_bytes(self.conn.take_sent()); - self.dec.collect_into_vec::>() + 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) { diff --git a/crates/valence/src/weather.rs b/crates/valence/src/weather.rs index badcc7f..bd27866 100644 --- a/crates/valence/src/weather.rs +++ b/crates/valence/src/weather.rs @@ -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(()) } } diff --git a/crates/valence_anvil/examples/anvil_loading.rs b/crates/valence_anvil/examples/anvil_loading.rs index cf43436..72e52ea 100644 --- a/crates/valence_anvil/examples/anvil_loading.rs +++ b/crates/valence_anvil/examples/anvil_loading.rs @@ -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>, + mut clients: Query<(&mut Location, &mut Position, &mut GameMode, &mut IsFlat), Added>, instances: Query>, - 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() - }); } } diff --git a/crates/valence_protocol/benches/benches.rs b/crates/valence_protocol/benches/benches.rs index 18634bf..5b8ca8c 100644 --- a/crates/valence_protocol/benches/benches.rs +++ b/crates/valence_protocol/benches/benches.rs @@ -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::().unwrap(); + decode_packet::(&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::().unwrap(); + decode_packet::(&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::().unwrap(); + decode_packet::(&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::().unwrap(); + decode_packet::(&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::().unwrap(); + decode_packet::(&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::().unwrap(); + decode_packet::(&decoder.try_next_packet().unwrap().unwrap()).unwrap(); black_box(decoder); }); diff --git a/crates/valence_protocol/src/codec.rs b/crates/valence_protocol/src/codec.rs deleted file mode 100644 index e5432a2..0000000 --- a/crates/valence_protocol/src/codec.rs +++ /dev/null @@ -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; - -#[derive(Default)] -pub struct PacketEncoder { - buf: BytesMut, - #[cfg(feature = "compression")] - compress_buf: Vec, - #[cfg(feature = "compression")] - compression_threshold: Option, - #[cfg(feature = "encryption")] - cipher: Option, -} - -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) { - 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, 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, - pkt: &P, - threshold: u32, - scratch: &mut Vec, -) -> 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, - #[cfg(feature = "compression")] - compression_enabled: bool, - #[cfg(feature = "encryption")] - cipher: Option, -} - -impl PacketDecoder { - pub fn new() -> Self { - Self::default() - } - - pub fn try_next_packet<'a, P>(&'a mut self) -> Result> - 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> - 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 { - 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>, - i: Option, - 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::() - .unwrap() - .unwrap() - .check("first"); - #[cfg(feature = "compression")] - dec.set_compression(true); - dec.try_next_packet::() - .unwrap() - .unwrap() - .check("second"); - #[cfg(feature = "encryption")] - dec.enable_encryption(&CRYPT_KEY); - dec.try_next_packet::() - .unwrap() - .unwrap() - .check("fourth"); - dec.try_next_packet::() - .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::().unwrap(); - - assert_eq!(packets, res); - } -} diff --git a/crates/valence_protocol/src/decoder.rs b/crates/valence_protocol/src/decoder.rs new file mode 100644 index 0000000..030b3c3 --- /dev/null +++ b/crates/valence_protocol/src/decoder.rs @@ -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; + +#[derive(Default)] +pub struct PacketDecoder { + buf: BytesMut, + #[cfg(feature = "compression")] + decompress_buf: BytesMut, + #[cfg(feature = "compression")] + compression_threshold: Option, + #[cfg(feature = "encryption")] + cipher: Option, +} + +impl PacketDecoder { + pub fn new() -> Self { + Self::default() + } + + pub fn try_next_packet(&mut self) -> Result> { + 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 { + self.compression_threshold + } + + #[cfg(feature = "compression")] + pub fn set_compression(&mut self, threshold: Option) { + 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

{ + let pkt = P::decode_packet(&mut bytes)?; + + ensure!( + bytes.is_empty(), + "missed {} bytes while decoding {}", + bytes.len(), + pkt.packet_name() + ); + + Ok(pkt) +} diff --git a/crates/valence_protocol/src/encoder.rs b/crates/valence_protocol/src/encoder.rs new file mode 100644 index 0000000..5c26292 --- /dev/null +++ b/crates/valence_protocol/src/encoder.rs @@ -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; + +#[derive(Default)] +pub struct PacketEncoder { + buf: BytesMut, + #[cfg(feature = "compression")] + compress_buf: Vec, + #[cfg(feature = "compression")] + compression_threshold: Option, + #[cfg(feature = "encryption")] + cipher: Option, +} + +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) { + 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, 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, + pkt: &P, + threshold: u32, + scratch: &mut Vec, +) -> 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(()) +} diff --git a/crates/valence_protocol/src/impls.rs b/crates/valence_protocol/src/impls.rs index 3a2eaed..8c1bfcb 100644 --- a/crates/valence_protocol/src/impls.rs +++ b/crates/valence_protocol/src/impls.rs @@ -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 { 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 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 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 { diff --git a/crates/valence_protocol/src/item.rs b/crates/valence_protocol/src/item.rs index 2963b08..95f9bb6 100644 --- a/crates/valence_protocol/src/item.rs +++ b/crates/valence_protocol/src/item.rs @@ -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) -> 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>) -> Self { + self.nbt = nbt.into(); + self + } + /// Gets the number of items in this stack. pub fn count(&self) -> u8 { self.count diff --git a/crates/valence_protocol/src/lib.rs b/crates/valence_protocol/src/lib.rs index 62e07a9..edb0fda 100644 --- a/crates/valence_protocol/src/lib.rs +++ b/crates/valence_protocol/src/lib.rs @@ -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::().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>, + i: Option, + 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::(&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"); + } } diff --git a/crates/valence_protocol/src/packet/c2s/play/click_slot.rs b/crates/valence_protocol/src/packet/c2s/play/click_slot.rs index fe5de83..1d6d76b 100644 --- a/crates/valence_protocol/src/packet/c2s/play/click_slot.rs +++ b/crates/valence_protocol/src/packet/c2s/play/click_slot.rs @@ -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, + pub slot_changes: Vec, pub carried_item: Option, } diff --git a/crates/valence_protocol/src/packet/c2s/play/client_settings.rs b/crates/valence_protocol/src/packet/c2s/play/client_settings.rs index fa6025e..c2ba747 100644 --- a/crates/valence_protocol/src/packet/c2s/play/client_settings.rs +++ b/crates/valence_protocol/src/packet/c2s/play/client_settings.rs @@ -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, } diff --git a/crates/valence_protocol/src/var_int.rs b/crates/valence_protocol/src/var_int.rs index a609348..a60f886 100644 --- a/crates/valence_protocol/src/var_int.rs +++ b/crates/valence_protocol/src/var_int.rs @@ -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, diff --git a/crates/valence_protocol_macros/src/lib.rs b/crates/valence_protocol_macros/src/lib.rs index 8cad5a4..1b58093 100644 --- a/crates/valence_protocol_macros/src/lib.rs +++ b/crates/valence_protocol_macros/src/lib.rs @@ -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. diff --git a/crates/valence_stresser/src/stresser.rs b/crates/valence_stresser/src/stresser.rs index 42d0e09..211851d 100644 --- a/crates/valence_stresser/src/stresser.rs +++ b/crates/valence_stresser/src/stresser.rs @@ -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::() { - 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::(&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::() { - 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); } }