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); } }