From 58f819791317016ece978b8b3cf815bb5edb2742 Mon Sep 17 00:00:00 2001 From: Ryan Johnson Date: Tue, 29 Nov 2022 03:37:32 -0800 Subject: [PATCH] Client Cleanup (#159) # Noteworthy Changes - Simplified the `client` module and update procedure. Sending packets is not deferred unless necessary. - Client events are no longer buffered in `Client` before reaching the user. - Expanded `ClientEvent` to account for most packets. - Most types containing custom `state` now implement `Deref` and `DerefMut`. This means you don't have to write `.state` all over the place. `Server` was excluded from this because it does not play well with the borrow checker. - Fixed bugs related to entity visibility. - Client now correctly holds the semaphore permit from the initial connection. - Other miscellaneous API changes throughout the project. # Known Issues - Inventory stuff is still incomplete. The inventory examples have been temporarily disabled. --- examples/biomes.rs | 12 +- examples/building.rs | 114 +- examples/chest.rs | 62 +- examples/combat.rs | 102 +- examples/conway.rs | 55 +- examples/cow_sphere.rs | 28 +- examples/death.rs | 88 +- examples/entity_raycast.rs | 43 +- examples/inventory_piano.rs | 9 +- examples/parkour.rs | 137 +- examples/resource_pack.rs | 61 +- examples/terrain.rs | 33 +- examples/text.rs | 25 +- performance_tests/players/src/main.rs | 13 +- src/chunk.rs | 91 +- src/client.rs | 1160 ++++++----------- src/client/event.rs | 992 ++++++++++---- src/config.rs | 36 +- src/entity.rs | 121 +- src/inventory.rs | 414 +++--- src/lib.rs | 11 +- src/player_list.rs | 54 +- src/server.rs | 120 +- src/server/login.rs | 14 +- ...packet_controller.rs => packet_manager.rs} | 109 +- src/slab_rc.rs | 4 +- src/world.rs | 22 +- valence_protocol/src/ident.rs | 39 +- valence_protocol/src/inventory.rs | 62 + valence_protocol/src/item.rs | 14 +- valence_protocol/src/lib.rs | 2 + valence_protocol/src/packets/c2s.rs | 33 +- valence_protocol/src/packets/s2c.rs | 26 +- valence_protocol/src/types.rs | 2 +- 34 files changed, 2162 insertions(+), 1946 deletions(-) rename src/server/{packet_controller.rs => packet_manager.rs} (85%) create mode 100644 valence_protocol/src/inventory.rs diff --git a/examples/biomes.rs b/examples/biomes.rs index a77f287..03f5a7a 100644 --- a/examples/biomes.rs +++ b/examples/biomes.rs @@ -40,6 +40,7 @@ impl Config for Game { type WorldState = (); type ChunkState = (); type PlayerListState = (); + type InventoryState = (); fn dimensions(&self) -> Vec { vec![Dimension { @@ -152,14 +153,17 @@ impl Config for Game { .entities .insert_with_uuid(EntityKind::Player, client.uuid(), ()) { - Some((id, _)) => client.state.entity_id = id, + Some((id, entity)) => { + entity.set_world(world_id); + client.entity_id = id + } None => { client.disconnect("Conflicting UUID"); return false; } } - client.spawn(world_id); + client.respawn(world_id); client.set_flat(true); client.teleport(spawn_pos, 0.0, 0.0); client.set_player_list(server.state.player_list.clone()); @@ -178,9 +182,11 @@ impl Config for Game { client.set_game_mode(GameMode::Creative); } + while client.next_event().is_some() {} + if client.is_disconnected() { self.player_count.fetch_sub(1, Ordering::SeqCst); - server.entities.remove(client.state.entity_id); + server.entities.remove(client.entity_id); if let Some(id) = &server.state.player_list { server.player_lists.get_mut(id).remove(client.uuid()); } diff --git a/examples/building.rs b/examples/building.rs index 86c5f1a..454dda6 100644 --- a/examples/building.rs +++ b/examples/building.rs @@ -1,8 +1,6 @@ use std::net::SocketAddr; use std::sync::atomic::{AtomicUsize, Ordering}; -use num::Integer; -use valence::client::DiggingStatus; use valence::prelude::*; pub fn main() -> ShutdownResult { @@ -31,8 +29,8 @@ struct ClientState { const MAX_PLAYERS: usize = 10; -const SIZE_X: usize = 100; -const SIZE_Z: usize = 100; +const SIZE_X: i32 = 100; +const SIZE_Z: i32 = 100; #[async_trait] impl Config for Game { @@ -42,6 +40,7 @@ impl Config for Game { type WorldState = (); type ChunkState = (); type PlayerListState = (); + type InventoryState = (); fn dimensions(&self) -> Vec { vec![Dimension { @@ -70,33 +69,11 @@ impl Config for Game { server.state.player_list = Some(server.player_lists.insert(()).0); // initialize chunks - for chunk_z in -2..Integer::div_ceil(&(SIZE_Z as i32), &16) + 2 { - for chunk_x in -2..Integer::div_ceil(&(SIZE_X as i32), &16) + 2 { - world.chunks.insert( - [chunk_x as i32, chunk_z as i32], - UnloadedChunk::default(), - (), - ); - } - } - - // initialize blocks in the chunks - for chunk_x in 0..Integer::div_ceil(&SIZE_X, &16) { - for chunk_z in 0..Integer::div_ceil(&SIZE_Z, &16) { - let chunk = world + for z in 0..SIZE_Z { + for x in 0..SIZE_X { + world .chunks - .get_mut((chunk_x as i32, chunk_z as i32)) - .unwrap(); - for x in 0..16 { - for z in 0..16 { - let cell_x = chunk_x * 16 + x; - let cell_z = chunk_z * 16 + z; - - if cell_x < SIZE_X && cell_z < SIZE_Z { - chunk.set_block_state(x, 63, z, BlockState::GRASS_BLOCK); - } - } - } + .set_block_state([x, 0, z], BlockState::GRASS_BLOCK); } } } @@ -123,14 +100,17 @@ impl Config for Game { .entities .insert_with_uuid(EntityKind::Player, client.uuid(), ()) { - Some((id, _)) => client.state.entity_id = id, + Some((id, entity)) => { + entity.set_world(world_id); + client.entity_id = id + } None => { client.disconnect("Conflicting UUID"); return false; } } - client.spawn(world_id); + client.respawn(world_id); client.set_flat(true); client.teleport(spawn_pos, 0.0, 0.0); client.set_player_list(server.state.player_list.clone()); @@ -150,46 +130,24 @@ impl Config for Game { client.send_message("Welcome to Valence! Build something cool.".italic()); } - if client.is_disconnected() { - self.player_count.fetch_sub(1, Ordering::SeqCst); - server.entities.remove(client.state.entity_id); - if let Some(id) = &server.state.player_list { - server.player_lists.get_mut(id).remove(client.uuid()); - } - return false; - } + let player = server.entities.get_mut(client.entity_id).unwrap(); - let player = server.entities.get_mut(client.state.entity_id).unwrap(); - - if client.position().y <= -20.0 { - client.teleport(spawn_pos, client.yaw(), client.pitch()); - } - - while let Some(event) = handle_event_default(client, player) { + while let Some(event) = client.next_event() { + event.handle_default(client, player); match event { - ClientEvent::Digging { - position, status, .. - } => { - match status { - DiggingStatus::Start => { - // Allows clients in creative mode to break blocks. - if client.game_mode() == GameMode::Creative { - world.chunks.set_block_state(position, BlockState::AIR); - } - } - DiggingStatus::Finish => { - // Allows clients in survival mode to break blocks. - world.chunks.set_block_state(position, BlockState::AIR); - } - _ => {} + ClientEvent::StartDigging { position, .. } => { + // Allows clients in creative mode to break blocks. + if client.game_mode() == GameMode::Creative { + world.chunks.set_block_state(position, BlockState::AIR); } } - ClientEvent::InteractWithBlock { - hand, - location, - face, - .. - } => { + ClientEvent::FinishDigging { position, .. } => { + // Allows clients in survival mode to break blocks. + world.chunks.set_block_state(position, BlockState::AIR); + } + ClientEvent::UseItemOnBlock { .. } => { + // TODO: reimplement when inventories are re-added. + /* if hand == Hand::Main { if let Some(stack) = client.held_item() { if let Some(held_block_kind) = stack.item.to_block_kind() { @@ -200,24 +158,38 @@ impl Config for Game { { if world .chunks - .block_state(location) + .block_state(position) .map(|s| s.is_replaceable()) .unwrap_or(false) { - world.chunks.set_block_state(location, block_to_place); + world.chunks.set_block_state(position, block_to_place); } else { - let place_at = location.get_in_direction(face); + let place_at = position.get_in_direction(face); world.chunks.set_block_state(place_at, block_to_place); } } } } } + */ } _ => {} } } + if client.is_disconnected() { + self.player_count.fetch_sub(1, Ordering::SeqCst); + server.entities.remove(client.entity_id); + if let Some(id) = &server.state.player_list { + server.player_lists.get_mut(id).remove(client.uuid()); + } + return false; + } + + if client.position().y <= -20.0 { + client.teleport(spawn_pos, client.yaw(), client.pitch()); + } + true }); } diff --git a/examples/chest.rs b/examples/chest.rs index e33fe53..1fc3da0 100644 --- a/examples/chest.rs +++ b/examples/chest.rs @@ -1,3 +1,8 @@ +pub fn main() { + todo!("reimplement when inventories are re-added"); +} + +/* use std::net::SocketAddr; use std::sync::atomic::{AtomicUsize, Ordering}; @@ -49,6 +54,7 @@ impl Config for Game { type WorldState = (); type ChunkState = (); type PlayerListState = (); + type InventoryState = (); fn dimensions(&self) -> Vec { vec![Dimension { @@ -101,7 +107,11 @@ impl Config for Game { // create chest inventory let inv = ConfigurableInventory::new(27, VarInt(2), None); - let (id, _inv) = server.inventories.insert(inv); + let title = "Extra".italic() + + " Chesty".not_italic().bold().color(Color::RED) + + " Chest".not_italic(); + + let (id, _inv) = server.inventories.insert(inv, title, ()); server.state.chest = id; } @@ -137,14 +147,17 @@ impl Config for Game { .entities .insert_with_uuid(EntityKind::Player, client.uuid(), ()) { - Some((id, _)) => client.state.entity_id = id, + Some((id, entity)) => { + entity.set_world(world_id); + client.state.entity_id = id + } None => { client.disconnect("Conflicting UUID"); return false; } } - client.spawn(world_id); + client.respawn(world_id); client.set_flat(true); client.teleport(spawn_pos, 0.0, 0.0); client.set_player_list(server.state.player_list.clone()); @@ -163,35 +176,17 @@ impl Config for Game { client.send_message("Welcome to Valence! Sneak to give yourself an item.".italic()); } - if client.is_disconnected() { - self.player_count.fetch_sub(1, Ordering::SeqCst); - server.entities.remove(client.state.entity_id); - if let Some(id) = &server.state.player_list { - server.player_lists.get_mut(id).remove(client.uuid()); - } - return false; - } - let player = server.entities.get_mut(client.state.entity_id).unwrap(); - if client.position().y <= -20.0 { - client.teleport(spawn_pos, client.yaw(), client.pitch()); - } - - while let Some(event) = handle_event_default(client, player) { + while let Some(event) = client.next_event() { + event.handle_default(client, player); match event { - ClientEvent::InteractWithBlock { hand, location, .. } => { + ClientEvent::UseItemOnBlock { hand, position, .. } => { if hand == Hand::Main - && world.chunks.block_state(location) == Some(BlockState::CHEST) + && world.chunks.block_state(position) == Some(BlockState::CHEST) { client.send_message("Opening chest!"); - client.open_inventory( - &server.inventories, - server.state.chest, - "Extra".italic() - + " Chesty".not_italic().bold().color(Color::RED) - + " Chest".not_italic(), - ); + client.open_inventory(server.state.chest); } } ClientEvent::CloseScreen { window_id } => { @@ -207,6 +202,7 @@ impl Config for Game { mode, slot_changes, carried_item, + .. } => { println!( "window_id: {:?}, state_id: {:?}, slot_id: {:?}, mode: {:?}, \ @@ -244,6 +240,19 @@ impl Config for Game { } } + if client.is_disconnected() { + self.player_count.fetch_sub(1, Ordering::SeqCst); + server.entities.remove(client.state.entity_id); + if let Some(id) = &server.state.player_list { + server.player_lists.get_mut(id).remove(client.uuid()); + } + return false; + } + + if client.position().y <= -20.0 { + client.teleport(spawn_pos, client.yaw(), client.pitch()); + } + true }); } @@ -256,3 +265,4 @@ fn rotate_items(inv: &mut ConfigurableInventory) { inv.set_slot((i - 1) as SlotId, b); } } +*/ diff --git a/examples/combat.rs b/examples/combat.rs index b4ad6d8..2447c6f 100644 --- a/examples/combat.rs +++ b/examples/combat.rs @@ -47,6 +47,7 @@ impl Config for Game { type WorldState = (); type ChunkState = (); type PlayerListState = (); + type InventoryState = (); async fn server_list_ping( &self, @@ -128,12 +129,12 @@ impl Config for Game { } }; - player.state.client = client_id; + player.set_world(world_id); + player.client = client_id; - client.state.player = player_id; - client.state.extra_knockback = true; + client.player = player_id; - client.spawn(world_id); + client.respawn(world_id); client.set_flat(true); client.set_game_mode(GameMode::Survival); client.teleport( @@ -164,9 +165,42 @@ impl Config for Game { } } + while let Some(event) = client.next_event() { + let player = server + .entities + .get_mut(client.player) + .expect("missing player entity"); + + event.handle_default(client, player); + match event { + ClientEvent::StartSprinting => { + client.extra_knockback = true; + } + ClientEvent::StopSprinting => { + client.extra_knockback = false; + } + ClientEvent::InteractWithEntity { entity_id, .. } => { + if let Some((id, target)) = server.entities.get_with_raw_id_mut(entity_id) { + if !target.attacked + && current_tick - target.last_attack_time >= 10 + && id != client.player + { + target.attacked = true; + target.attacker_pos = client.position(); + target.extra_knockback = client.extra_knockback; + target.last_attack_time = current_tick; + + client.extra_knockback = false; + } + } + } + _ => {} + } + } + if client.is_disconnected() { self.player_count.fetch_sub(1, Ordering::SeqCst); - server.entities.remove(client.state.player); + server.entities.remove(client.player); if let Some(id) = &server.state { server.player_lists.get_mut(id).remove(client.uuid()); } @@ -185,70 +219,28 @@ impl Config for Game { ); } - loop { - let player = server - .entities - .get_mut(client.state.player) - .expect("missing player entity"); - - match handle_event_default(client, player) { - Some(ClientEvent::StartSprinting) => { - client.state.extra_knockback = true; - } - Some(ClientEvent::StopSprinting) => { - client.state.extra_knockback = false; - } - Some(ClientEvent::InteractWithEntity { id, .. }) => { - if let Some(target) = server.entities.get_mut(id) { - if !target.state.attacked - && current_tick - target.state.last_attack_time >= 10 - && id != client.state.player - { - target.state.attacked = true; - target.state.attacker_pos = client.position(); - target.state.extra_knockback = client.state.extra_knockback; - target.state.last_attack_time = current_tick; - - client.state.extra_knockback = false; - } - } - } - Some(_) => {} - None => break, - } - } - true }); for (_, entity) in server.entities.iter_mut() { - if entity.state.attacked { - entity.state.attacked = false; - if let Some(victim) = server.clients.get_mut(entity.state.client) { + if entity.attacked { + entity.attacked = false; + if let Some(victim) = server.clients.get_mut(entity.client) { let victim_pos = Vec2::new(victim.position().x, victim.position().z); - let attacker_pos = - Vec2::new(entity.state.attacker_pos.x, entity.state.attacker_pos.z); + let attacker_pos = Vec2::new(entity.attacker_pos.x, entity.attacker_pos.z); let dir = (victim_pos - attacker_pos).normalized(); - let knockback_xz = if entity.state.extra_knockback { - 18.0 - } else { - 8.0 - }; - let knockback_y = if entity.state.extra_knockback { - 8.432 - } else { - 6.432 - }; + let knockback_xz = if entity.extra_knockback { 18.0 } else { 8.0 }; + let knockback_y = if entity.extra_knockback { 8.432 } else { 6.432 }; let vel = Vec3::new(dir.x * knockback_xz, knockback_y, dir.y * knockback_xz); victim.set_velocity(vel.as_()); entity.push_event(EntityEvent::DamageFromGenericSource); entity.push_event(EntityEvent::Damage); - victim.push_entity_event(EntityEvent::DamageFromGenericSource); - victim.push_entity_event(EntityEvent::Damage); + victim.send_entity_event(EntityEvent::DamageFromGenericSource); + victim.send_entity_event(EntityEvent::Damage); } } } diff --git a/examples/conway.rs b/examples/conway.rs index de36de4..c56810a 100644 --- a/examples/conway.rs +++ b/examples/conway.rs @@ -1,5 +1,5 @@ use std::mem; -use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4}; +use std::net::SocketAddr; use std::sync::atomic::{AtomicUsize, Ordering}; use num::Integer; @@ -52,10 +52,7 @@ impl Config for Game { type WorldState = (); type ChunkState = (); type PlayerListState = (); - - fn address(&self) -> SocketAddr { - SocketAddrV4::new(Ipv4Addr::new(0, 0, 0, 0), 25565).into() // TODO remove - } + type InventoryState = (); fn dimensions(&self) -> Vec { vec![Dimension { @@ -128,14 +125,17 @@ impl Config for Game { .entities .insert_with_uuid(EntityKind::Player, client.uuid(), ()) { - Some((id, _)) => client.state.entity_id = id, + Some((id, entity)) => { + entity.set_world(world_id); + client.entity_id = id + } None => { client.disconnect("Conflicting UUID"); return false; } } - client.spawn(world_id); + client.respawn(world_id); client.set_flat(true); client.teleport(spawn_pos, 0.0, 0.0); client.set_player_list(server.state.player_list.clone()); @@ -157,25 +157,12 @@ impl Config for Game { ); } - if client.is_disconnected() { - self.player_count.fetch_sub(1, Ordering::SeqCst); - server.entities.remove(client.state.entity_id); - if let Some(id) = &server.state.player_list { - server.player_lists.get_mut(id).remove(client.uuid()); - } - return false; - } + let player = server.entities.get_mut(client.entity_id).unwrap(); - let player = server.entities.get_mut(client.state.entity_id).unwrap(); - - if client.position().y <= 0.0 { - client.teleport(spawn_pos, client.yaw(), client.pitch()); - server.state.board.fill(false); - } - - while let Some(event) = handle_event_default(client, player) { + while let Some(event) = client.next_event() { + event.handle_default(client, player); match event { - ClientEvent::Digging { position, .. } => { + ClientEvent::StartDigging { position, .. } => { if (0..SIZE_X as i32).contains(&position.x) && (0..SIZE_Z as i32).contains(&position.z) && position.y == BOARD_Y @@ -195,7 +182,7 @@ impl Config for Game { server.state.board[index] = true; } } - ClientEvent::InteractWithBlock { hand, .. } => { + ClientEvent::UseItemOnBlock { hand, .. } => { if hand == Hand::Main { client.send_message("I said left click, not right click!".italic()); } @@ -204,6 +191,20 @@ impl Config for Game { } } + if client.is_disconnected() { + self.player_count.fetch_sub(1, Ordering::SeqCst); + server.entities.remove(client.entity_id); + if let Some(id) = &server.state.player_list { + server.player_lists.get_mut(id).remove(client.uuid()); + } + return false; + } + + if client.position().y <= 0.0 { + client.teleport(spawn_pos, client.yaw(), client.pitch()); + server.state.board.fill(false); + } + if let TrackedData::Player(data) = player.data() { let sneaking = data.get_pose() == Pose::Sneaking; if sneaking != server.state.paused { @@ -212,8 +213,8 @@ impl Config for Game { Ident::new("block.note_block.pling").unwrap(), SoundCategory::Block, client.position(), - 0.5f32, - if sneaking { 0.5f32 } else { 1f32 }, + 0.5, + if sneaking { 0.5 } else { 1.0 }, ); } } diff --git a/examples/cow_sphere.rs b/examples/cow_sphere.rs index b3617ca..37d7251 100644 --- a/examples/cow_sphere.rs +++ b/examples/cow_sphere.rs @@ -3,7 +3,6 @@ use std::f64::consts::TAU; use std::net::SocketAddr; use std::sync::atomic::{AtomicUsize, Ordering}; -use uuid::Uuid; use valence::prelude::*; pub fn main() -> ShutdownResult { @@ -29,6 +28,11 @@ struct ServerState { cows: Vec, } +#[derive(Default)] +struct ClientState { + entity_id: EntityId, +} + const MAX_PLAYERS: usize = 10; const SPAWN_POS: BlockPos = BlockPos::new(0, 100, -25); @@ -36,11 +40,12 @@ const SPAWN_POS: BlockPos = BlockPos::new(0, 100, -25); #[async_trait] impl Config for Game { type ServerState = ServerState; - type ClientState = EntityId; + type ClientState = ClientState; type EntityState = (); type WorldState = (); type ChunkState = (); type PlayerListState = (); + type InventoryState = (); async fn server_list_ping( &self, @@ -108,14 +113,17 @@ impl Config for Game { .entities .insert_with_uuid(EntityKind::Player, client.uuid(), ()) { - Some((id, _)) => client.state = id, + Some((id, entity)) => { + entity.set_world(world_id); + client.entity_id = id + } None => { client.disconnect("Conflicting UUID"); return false; } } - client.spawn(world_id); + client.respawn(world_id); client.set_flat(true); client.set_game_mode(GameMode::Creative); client.teleport( @@ -146,17 +154,19 @@ impl Config for Game { if let Some(id) = &server.state.player_list { server.player_lists.get_mut(id).remove(client.uuid()); } - server.entities.remove(client.state); + server.entities.remove(client.entity_id); return false; } let entity = server .entities - .get_mut(client.state) + .get_mut(client.entity_id) .expect("missing player entity"); - while handle_event_default(client, entity).is_some() {} + while let Some(event) = client.next_event() { + event.handle_default(client, entity); + } true }); @@ -204,9 +214,9 @@ impl Config for Game { /// Distributes N points on the surface of a unit sphere. fn fibonacci_spiral(n: usize) -> impl Iterator> { - (0..n).map(move |i| { - let golden_ratio = (1.0 + 5_f64.sqrt()) / 2.0; + let golden_ratio = (1.0 + 5_f64.sqrt()) / 2.0; + (0..n).map(move |i| { // Map to unit square let x = i as f64 / golden_ratio % 1.0; let y = i as f64 / n as f64; diff --git a/examples/death.rs b/examples/death.rs index 6724bd5..ab930a8 100644 --- a/examples/death.rs +++ b/examples/death.rs @@ -27,12 +27,9 @@ struct ClientState { can_respawn: bool, } -struct WorldState { - player_list: PlayerListId, -} - #[derive(Default)] struct ServerState { + player_list: Option, first_world: WorldId, second_world: WorldId, third_world: WorldId, @@ -71,9 +68,10 @@ impl Config for Game { type ServerState = ServerState; type ClientState = ClientState; type EntityState = (); - type WorldState = WorldState; + type WorldState = (); type ChunkState = (); type PlayerListState = (); + type InventoryState = (); fn dimensions(&self) -> Vec { vec![ @@ -107,6 +105,7 @@ impl Config for Game { // We created server with meaningless default state. // Let's create three worlds and create new ServerState. server.state = ServerState { + player_list: Some(server.player_lists.insert(()).0), first_world: create_world(server, FIRST_WORLD_SPAWN_BLOCK, WhichWorld::First), second_world: create_world(server, SECOND_WORLD_SPAWN_BLOCK, WhichWorld::Second), third_world: create_world(server, THIRD_WORLD_SPAWN_BLOCK, WhichWorld::Third), @@ -131,17 +130,17 @@ impl Config for Game { .entities .insert_with_uuid(EntityKind::Player, client.uuid(), ()) { - Some((id, _)) => client.state.entity_id = id, + Some((id, entity)) => { + entity.set_world(server.state.first_world); + client.entity_id = id + } None => { client.disconnect("Conflicting UUID"); return false; } } - let first_world_id = server.state.first_world; - let first_world = server.worlds.get(first_world_id).unwrap(); - - client.state.respawn_location = ( + client.respawn_location = ( server.state.first_world, block_pos_to_vec(FIRST_WORLD_SPAWN_BLOCK), ); @@ -150,14 +149,14 @@ impl Config for Game { client.set_spawn_position(FIRST_WORLD_SPAWN_BLOCK, 0.0); client.set_flat(true); - client.spawn(first_world_id); - client.teleport(client.state.respawn_location.1, 0.0, 0.0); + client.respawn(server.state.first_world); + client.teleport(client.respawn_location.1, 0.0, 0.0); - client.set_player_list(first_world.state.player_list.clone()); + client.set_player_list(server.state.player_list.clone()); server .player_lists - .get_mut(&first_world.state.player_list) + .get_mut(server.state.player_list.as_ref().unwrap()) .insert( client.uuid(), client.username(), @@ -178,28 +177,17 @@ impl Config for Game { // TODO after inventory support is added, show interaction with compass. - if client.is_disconnected() { - self.player_count.fetch_sub(1, Ordering::SeqCst); - server.entities.remove(client.state.entity_id); - - if let Some(list) = client.player_list() { - server.player_lists.get_mut(list).remove(client.uuid()); - } - - return false; - } - // Handling respawn locations - if !client.state.can_respawn { + if !client.can_respawn { if client.position().y < 0.0 { - client.state.can_respawn = true; + client.can_respawn = true; client.kill(None, "You fell"); // You could have also killed the player with `Client::set_health_and_food`, // however you cannot send a message to the death screen // that way if client.world() == server.state.third_world { // Falling in third world gets you back to the first world - client.state.respawn_location = ( + client.respawn_location = ( server.state.first_world, block_pos_to_vec(FIRST_WORLD_SPAWN_BLOCK), ); @@ -207,7 +195,7 @@ impl Config for Game { } else { // falling in first and second world will cause player to spawn in third // world - client.state.respawn_location = ( + client.respawn_location = ( server.state.third_world, block_pos_to_vec(THIRD_WORLD_SPAWN_BLOCK), ); @@ -222,15 +210,15 @@ impl Config for Game { if client.position().x >= LEFT_DEATH_LINE as f64 { // Client went to the left, he dies - client.state.can_respawn = true; + client.can_respawn = true; client.kill(None, death_msg); } if client.position().x <= RIGHT_DEATH_LINE as f64 { // Client went to the right, he dies and spawns in world2 - client.state.can_respawn = true; + client.can_respawn = true; client.kill(None, death_msg); - client.state.respawn_location = ( + client.respawn_location = ( server.state.second_world, block_pos_to_vec(SECOND_WORLD_SPAWN_BLOCK), ); @@ -239,33 +227,46 @@ impl Config for Game { } } - let player = server.entities.get_mut(client.state.entity_id).unwrap(); + let player = server.entities.get_mut(client.entity_id).unwrap(); - while let Some(event) = handle_event_default(client, player) { + while let Some(event) = client.next_event() { + event.handle_default(client, player); match event { - ClientEvent::RespawnRequest => { - if !client.state.can_respawn { - client.disconnect("Unexpected RespawnRequest"); + ClientEvent::PerformRespawn => { + if !client.can_respawn { + client.disconnect("Unexpected PerformRespawn"); return false; } // Let's respawn our player. `spawn` will load the world, but we are // responsible for teleporting the player. // You can store respawn however you want, for example in `Client`'s state. - let spawn = client.state.respawn_location; - client.spawn(spawn.0); + let spawn = client.respawn_location; + client.respawn(spawn.0); + player.set_world(spawn.0); client.teleport(spawn.1, 0.0, 0.0); - client.state.can_respawn = false; + client.can_respawn = false; } ClientEvent::StartSneaking => { // Roll the credits, respawn after - client.state.can_respawn = true; + client.can_respawn = true; client.win_game(true); } _ => {} } } + if client.is_disconnected() { + self.player_count.fetch_sub(1, Ordering::SeqCst); + server.entities.remove(client.entity_id); + + if let Some(list) = client.player_list() { + server.player_lists.get_mut(list).remove(client.uuid()); + } + + return false; + } + true }); } @@ -279,10 +280,7 @@ fn create_world(server: &mut Server, spawn_pos: BlockPos, world_type: Whic WhichWorld::Third => server.shared.dimensions().nth(1).unwrap(), }; - let player_list = server.player_lists.insert(()).0; - let (world_id, world) = server - .worlds - .insert(dimension.0, WorldState { player_list }); + let (world_id, world) = server.worlds.insert(dimension.0, ()); // Create chunks for chunk_z in -3..3 { diff --git a/examples/entity_raycast.rs b/examples/entity_raycast.rs index 95b7b7c..a5f919e 100644 --- a/examples/entity_raycast.rs +++ b/examples/entity_raycast.rs @@ -42,6 +42,7 @@ impl Config for Game { type WorldState = (); type ChunkState = (); type PlayerListState = (); + type InventoryState = (); async fn server_list_ping( &self, @@ -305,14 +306,17 @@ impl Config for Game { .entities .insert_with_uuid(EntityKind::Player, client.uuid(), ()) { - Some((id, _)) => client.state.player = id, + Some((id, entity)) => { + entity.set_world(world_id); + client.player = id + } None => { client.disconnect("Conflicting UUID"); return false; } } - client.spawn(world_id); + client.respawn(world_id); client.set_flat(true); client.set_game_mode(GameMode::Creative); client.teleport( @@ -344,12 +348,17 @@ impl Config for Game { ); } + let entity = server.entities.get_mut(client.player).unwrap(); + while let Some(event) = client.next_event() { + event.handle_default(client, entity); + } + if client.is_disconnected() { self.player_count.fetch_sub(1, Ordering::SeqCst); if let Some(id) = &server.state { server.player_lists.get_mut(id).remove(client.uuid()); } - server.entities.remove(client.state.player); + server.entities.remove(client.player); return false; } @@ -359,22 +368,21 @@ impl Config for Game { let origin = Vec3::new(client_pos.x, client_pos.y + PLAYER_EYE_HEIGHT, client_pos.z); let direction = from_yaw_and_pitch(client.yaw() as f64, client.pitch() as f64); let not_self_or_bullet = |hit: &RaycastHit| { - hit.entity != client.state.player && hit.entity != client.state.shulker_bullet + hit.entity != client.player && hit.entity != client.shulker_bullet }; if let Some(hit) = world .spatial_index .raycast(origin, direction, not_self_or_bullet) { - let bullet = - if let Some(bullet) = server.entities.get_mut(client.state.shulker_bullet) { - bullet - } else { - let (id, bullet) = server.entities.insert(EntityKind::ShulkerBullet, ()); - client.state.shulker_bullet = id; - bullet.set_world(world_id); - bullet - }; + let bullet = if let Some(bullet) = server.entities.get_mut(client.shulker_bullet) { + bullet + } else { + let (id, bullet) = server.entities.insert(EntityKind::ShulkerBullet, ()); + client.shulker_bullet = id; + bullet.set_world(world_id); + bullet + }; let mut hit_pos = origin + direction * hit.near; let hitbox = bullet.hitbox(); @@ -385,17 +393,10 @@ impl Config for Game { client.set_action_bar("Intersection".color(Color::GREEN)); } else { - server.entities.remove(client.state.shulker_bullet); + server.entities.remove(client.shulker_bullet); client.set_action_bar("No Intersection".color(Color::RED)); } - while handle_event_default( - client, - server.entities.get_mut(client.state.player).unwrap(), - ) - .is_some() - {} - true }); } diff --git a/examples/inventory_piano.rs b/examples/inventory_piano.rs index ff7ab35..665c07d 100644 --- a/examples/inventory_piano.rs +++ b/examples/inventory_piano.rs @@ -1,3 +1,8 @@ +pub fn main() { + todo!("reimplement when inventories are re-added"); +} + +/* use std::net::SocketAddr; use std::sync::atomic::{AtomicUsize, Ordering}; @@ -47,6 +52,7 @@ impl Config for Game { type WorldState = (); type ChunkState = (); type PlayerListState = (); + type InventoryState = (); fn dimensions(&self) -> Vec { vec![Dimension { @@ -135,7 +141,7 @@ impl Config for Game { } } - client.spawn(world_id); + client.respawn(world_id); client.set_flat(true); client.teleport(spawn_pos, 0.0, 0.0); client.set_player_list(server.state.player_list.clone()); @@ -227,3 +233,4 @@ fn play_note(client: &mut Client, player: &mut Entity, clicked_slot: }); } } +*/ diff --git a/examples/parkour.rs b/examples/parkour.rs index 789d73f..afaf1b5 100644 --- a/examples/parkour.rs +++ b/examples/parkour.rs @@ -26,6 +26,11 @@ struct ServerState { player_list: Option, } +#[derive(Default)] +struct ChunkState { + keep_loaded: bool, +} + #[derive(Default)] struct ClientState { entity_id: EntityId, @@ -56,9 +61,9 @@ impl Config for Game { type ClientState = ClientState; type EntityState = (); type WorldState = (); - /// If the chunk should stay loaded at the end of the tick. - type ChunkState = bool; + type ChunkState = ChunkState; type PlayerListState = (); + type InventoryState = (); async fn server_list_ping( &self, @@ -80,8 +85,6 @@ impl Config for Game { } fn update(&self, server: &mut Server) { - //let (world_id, world) = server.worlds.iter_mut().next().unwrap(); - server.clients.retain(|_, client| { if client.created_this_tick() { if self @@ -95,11 +98,15 @@ impl Config for Game { return false; } + let (world_id, world) = server.worlds.insert(DimensionId::default(), ()); + match server .entities .insert_with_uuid(EntityKind::Player, client.uuid(), ()) { - Some((id, _)) => { + Some((id, entity)) => { + entity.set_world(world_id); + // create client state client.state = ClientState { entity_id: id, @@ -108,32 +115,28 @@ impl Config for Game { combo: 0, last_block_timestamp: 0, target_y: 0, - world_id: WorldId::NULL, + world_id, }; } None => { client.disconnect("Conflicting UUID"); + server.worlds.remove(world_id); return false; } } - let (world_id, world) = server.worlds.insert(DimensionId::default(), ()); - - client.state.world_id = world_id; - for chunk_z in -1..3 { for chunk_x in -2..2 { world.chunks.insert( (chunk_x as i32, chunk_z as i32), UnloadedChunk::default(), - true, + ChunkState { keep_loaded: true }, ); } } - client.spawn(world_id); + client.respawn(world_id); client.set_flat(true); - // client.teleport(spawn_pos, 0.0, 0.0); client.set_player_list(server.state.player_list.clone()); if let Some(id) = &server.state.player_list { @@ -152,34 +155,19 @@ impl Config for Game { reset(client, world); } - let (world_id, world) = server - .worlds - .iter_mut() - .find(|w| w.0 == client.state.world_id) - .unwrap(); - - if client.is_disconnected() { - self.player_count.fetch_sub(1, Ordering::SeqCst); - server.entities.remove(client.state.entity_id); - if let Some(id) = &server.state.player_list { - server.player_lists.get_mut(id).remove(client.uuid()); - } - for block in &client.state.blocks { - world.chunks.set_block_state(*block, BlockState::AIR); - } - client.state.blocks.clear(); - client.state.score = 0; - - server.worlds.remove(world_id); - return false; - } + let world_id = client.world_id; + let world = server.worlds.get_mut(world_id).unwrap(); let p = client.position(); for pos in chunks_in_view_distance(ChunkPos::at(p.x, p.z), 3) { if let Some(chunk) = world.chunks.get_mut(pos) { - chunk.state = true; + chunk.keep_loaded = true; } else { - world.chunks.insert(pos, UnloadedChunk::default(), true); + world.chunks.insert( + pos, + UnloadedChunk::default(), + ChunkState { keep_loaded: true }, + ); } } @@ -187,7 +175,6 @@ impl Config for Game { client.send_message( "Your score was ".italic() + client - .state .score .to_string() .color(Color::GOLD) @@ -199,32 +186,31 @@ impl Config for Game { } let pos_under_player = BlockPos::new( - (client.position().x - 0.5f64).round() as i32, + (client.position().x - 0.5).round() as i32, client.position().y as i32 - 1, - (client.position().z - 0.5f64).round() as i32, + (client.position().z - 0.5).round() as i32, ); if let Some(index) = client - .state .blocks .iter() .position(|block| *block == pos_under_player) { if index > 0 { - let power_result = 2.0f32.powf((client.state.combo as f32) / 45.0); + let power_result = 2.0f32.powf((client.combo as f32) / 45.0); let max_time_taken = (1000.0f32 * (index as f32) / power_result) as u128; let current_time_millis = SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap() .as_millis(); - if current_time_millis - client.state.last_block_timestamp < max_time_taken { - client.state.combo += index as u32 + if current_time_millis - client.last_block_timestamp < max_time_taken { + client.combo += index as u32 } else { - client.state.combo = 0 + client.combo = 0 } - let pitch = 0.9 + ((client.state.combo as f32) - 1.0) * 0.05; + let pitch = 0.9 + ((client.combo as f32) - 1.0) * 0.05; for _ in 0..index { generate_next_block(client, world, true) @@ -239,12 +225,7 @@ impl Config for Game { ); client.set_title( "", - client - .state - .score - .to_string() - .color(Color::LIGHT_PURPLE) - .bold(), + client.score.to_string().color(Color::LIGHT_PURPLE).bold(), SetTitleAnimationTimes { fade_in: 0, stay: 7, @@ -254,23 +235,33 @@ impl Config for Game { } } - while handle_event_default( - client, - server.entities.get_mut(client.state.entity_id).unwrap(), - ) - .is_some() - {} + let player = server.entities.get_mut(client.entity_id).unwrap(); + + while let Some(event) = client.next_event() { + event.handle_default(client, player); + } // Remove chunks outside the view distance of players. world.chunks.retain(|_, chunk| { - if chunk.state { - chunk.state = false; + if chunk.keep_loaded { + chunk.keep_loaded = false; true } else { false } }); + if client.is_disconnected() { + self.player_count.fetch_sub(1, Ordering::SeqCst); + server.entities.remove(client.entity_id); + if let Some(id) = &server.state.player_list { + server.player_lists.get_mut(id).remove(client.uuid()); + } + + server.worlds.remove(world_id); + return false; + } + true }); } @@ -283,19 +274,19 @@ fn reset(client: &mut Client, world: &mut World) { world.chunks.insert( (chunk_x as i32, chunk_z as i32), UnloadedChunk::default(), - true, + ChunkState { keep_loaded: true }, ); } } - client.state.score = 0; - client.state.combo = 0; + client.score = 0; + client.combo = 0; - for block in &client.state.blocks { + for block in &client.blocks { world.chunks.set_block_state(*block, BlockState::AIR); } - client.state.blocks.clear(); - client.state.blocks.push_back(START_POS); + client.blocks.clear(); + client.blocks.push_back(START_POS); world.chunks.set_block_state(START_POS, BlockState::STONE); for _ in 0..10 { @@ -315,19 +306,19 @@ fn reset(client: &mut Client, world: &mut World) { fn generate_next_block(client: &mut Client, world: &mut World, in_game: bool) { if in_game { - let removed_block = client.state.blocks.pop_front().unwrap(); + let removed_block = client.blocks.pop_front().unwrap(); world.chunks.set_block_state(removed_block, BlockState::AIR); - client.state.score += 1 + client.score += 1 } - let last_pos = *client.state.blocks.back().unwrap(); - let block_pos = generate_random_block(last_pos, client.state.target_y); + let last_pos = *client.blocks.back().unwrap(); + let block_pos = generate_random_block(last_pos, client.target_y); if last_pos.y == START_POS.y { - client.state.target_y = 0 + client.target_y = 0 } else if last_pos.y < START_POS.y - 30 || last_pos.y > START_POS.y + 30 { - client.state.target_y = START_POS.y; + client.target_y = START_POS.y; } let mut rng = rand::thread_rng(); @@ -335,10 +326,10 @@ fn generate_next_block(client: &mut Client, world: &mut World, in_ga world .chunks .set_block_state(block_pos, *BLOCK_TYPES.choose(&mut rng).unwrap()); - client.state.blocks.push_back(block_pos); + client.blocks.push_back(block_pos); // Combo System - client.state.last_block_timestamp = SystemTime::now() + client.last_block_timestamp = SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap() .as_millis(); diff --git a/examples/resource_pack.rs b/examples/resource_pack.rs index 21238c3..4052f38 100644 --- a/examples/resource_pack.rs +++ b/examples/resource_pack.rs @@ -2,7 +2,6 @@ use std::net::SocketAddr; use std::sync::atomic::{AtomicUsize, Ordering}; use valence::prelude::*; -use valence_protocol::packets::c2s::play::ResourcePackC2s; use valence_protocol::types::EntityInteraction; pub fn main() -> ShutdownResult { @@ -14,7 +13,7 @@ pub fn main() -> ShutdownResult { }, ServerState { player_list: None, - sheep_id: None, + sheep_id: EntityId::NULL, }, ) } @@ -25,7 +24,7 @@ struct Game { struct ServerState { player_list: Option, - sheep_id: Option, + sheep_id: EntityId, } #[derive(Default)] @@ -45,6 +44,7 @@ impl Config for Game { type WorldState = (); type ChunkState = (); type PlayerListState = (); + type InventoryState = (); async fn server_list_ping( &self, @@ -73,7 +73,7 @@ impl Config for Game { } let (sheep_id, sheep) = server.entities.insert(EntityKind::Sheep, ()); - server.state.sheep_id = Some(sheep_id); + server.state.sheep_id = sheep_id; sheep.set_world(world_id); sheep.set_position([ SPAWN_POS.x as f64 + 0.5, @@ -108,14 +108,14 @@ impl Config for Game { .entities .insert_with_uuid(EntityKind::Player, client.uuid(), ()) { - Some((id, _)) => client.state.entity_id = id, + Some((id, _)) => client.entity_id = id, None => { client.disconnect("Conflicting UUID"); return false; } } - client.spawn(world_id); + client.respawn(world_id); client.set_flat(true); client.set_game_mode(GameMode::Creative); client.teleport( @@ -140,6 +140,10 @@ impl Config for Game { ); } + client.send_message( + "Hit the sheep above you to prompt for the resource pack again.".italic(), + ); + set_example_pack(client); } @@ -148,44 +152,37 @@ impl Config for Game { if let Some(id) = &server.state.player_list { server.player_lists.get_mut(id).remove(client.uuid()); } - server.entities.remove(client.state.entity_id); + server.entities.remove(client.entity_id); return false; } - let player = server.entities.get_mut(client.state.entity_id).unwrap(); + let player = server.entities.get_mut(client.entity_id).unwrap(); - while let Some(event) = handle_event_default(client, player) { + while let Some(event) = client.next_event() { + event.handle_default(client, player); match event { - ClientEvent::InteractWithEntity { id, interact, .. } => { + ClientEvent::InteractWithEntity { + entity_id, + interact, + .. + } => { if interact == EntityInteraction::Attack - && Some(id) == server.state.sheep_id + && entity_id == server.state.sheep_id.to_raw() { set_example_pack(client); } } - ClientEvent::ResourcePackStatusChanged(s) => { - let message = match s { - ResourcePackC2s::SuccessfullyLoaded => { - "The resource pack was successfully loaded!".color(Color::GREEN) - } - ResourcePackC2s::Declined => { - "You declined the resource pack :(".color(Color::RED) - } - ResourcePackC2s::FailedDownload => { - "The resource pack download failed.".color(Color::RED) - } - _ => continue, - }; - - client.send_message(message.italic()); - client.send_message( - "Hit the sheep above you to prompt the resource pack again." - .color(Color::GRAY) - .italic(), - ); + ClientEvent::ResourcePackLoaded => { + client.send_message("Resource pack loaded!".color(Color::GREEN)); } - _ => (), + ClientEvent::ResourcePackDeclined => { + client.send_message("Resource pack declined.".color(Color::RED)); + } + ClientEvent::ResourcePackFailedDownload => { + client.send_message("Resource pack download failed.".color(Color::RED)); + } + _ => {} } } diff --git a/examples/terrain.rs b/examples/terrain.rs index 93b36e1..5398eb6 100644 --- a/examples/terrain.rs +++ b/examples/terrain.rs @@ -50,6 +50,7 @@ impl Config for Game { /// If the chunk should stay loaded at the end of the tick. type ChunkState = bool; type PlayerListState = (); + type InventoryState = (); async fn server_list_ping( &self, @@ -91,14 +92,17 @@ impl Config for Game { .entities .insert_with_uuid(EntityKind::Player, client.uuid(), ()) { - Some((id, _)) => client.state = id, + Some((id, entity)) => { + entity.set_world(world_id); + client.state = id + } None => { client.disconnect("Conflicting UUID"); return false; } } - client.spawn(world_id); + client.respawn(world_id); client.set_flat(true); client.set_game_mode(GameMode::Creative); client.teleport([0.0, 200.0, 0.0], 0.0, 0.0); @@ -118,18 +122,9 @@ impl Config for Game { client.send_message("Welcome to the terrain example!".italic()); } - if client.is_disconnected() { - self.player_count.fetch_sub(1, Ordering::SeqCst); - if let Some(id) = &server.state { - server.player_lists.get_mut(id).remove(client.uuid()); - } - server.entities.remove(client.state); - - return false; - } - - if let Some(entity) = server.entities.get_mut(client.state) { - while handle_event_default(client, entity).is_some() {} + let player = server.entities.get_mut(client.state).unwrap(); + while let Some(event) = client.next_event() { + event.handle_default(client, player); } let dist = client.view_distance(); @@ -143,6 +138,16 @@ impl Config for Game { } } + if client.is_disconnected() { + self.player_count.fetch_sub(1, Ordering::SeqCst); + if let Some(id) = &server.state { + server.player_lists.get_mut(id).remove(client.uuid()); + } + server.entities.remove(client.state); + + return false; + } + true }); diff --git a/examples/text.rs b/examples/text.rs index 9e9940e..755f8a3 100644 --- a/examples/text.rs +++ b/examples/text.rs @@ -34,10 +34,7 @@ impl Config for Game { type WorldState = (); type ChunkState = (); type PlayerListState = (); - - fn max_connections(&self) -> usize { - 64 - } + type InventoryState = (); async fn server_list_ping( &self, @@ -68,7 +65,7 @@ impl Config for Game { .entities .insert_with_uuid(EntityKind::Player, client.uuid(), ()) { - Some((id, _)) => client.state.entity_id = id, + Some((id, _)) => client.entity_id = id, None => { client.disconnect("Conflicting UUID"); return false; @@ -78,7 +75,7 @@ impl Config for Game { let world_id = server.state.world; client.set_flat(true); - client.spawn(world_id); + client.respawn(world_id); client.teleport(SPAWN_POS, -90.0, 0.0); client.set_game_mode(GameMode::Creative); @@ -164,18 +161,20 @@ impl Config for Game { ); } - if client.is_disconnected() { - server.entities.remove(client.state.entity_id); - return false; - } - if client.position().y < 0.0 { client.teleport(SPAWN_POS, 0.0, 0.0); } - let player = server.entities.get_mut(client.state.entity_id).unwrap(); + let player = server.entities.get_mut(client.entity_id).unwrap(); - while handle_event_default(client, player).is_some() {} + while let Some(event) = client.next_event() { + event.handle_default(client, player); + } + + if client.is_disconnected() { + server.entities.remove(client.entity_id); + return false; + } true }); diff --git a/performance_tests/players/src/main.rs b/performance_tests/players/src/main.rs index 624be8e..af2d9fc 100644 --- a/performance_tests/players/src/main.rs +++ b/performance_tests/players/src/main.rs @@ -35,6 +35,7 @@ impl Config for Game { type WorldState = (); type ChunkState = (); type PlayerListState = (); + type InventoryState = (); fn max_connections(&self) -> usize { MAX_PLAYERS + 64 @@ -115,7 +116,7 @@ impl Config for Game { server.clients.retain(|_, client| { if client.created_this_tick() { - client.spawn(world_id); + client.respawn(world_id); client.set_flat(true); client.teleport([0.0, 1.0, 0.0], 0.0, 0.0); @@ -157,12 +158,14 @@ impl Config for Game { } if WITH_PLAYER_ENTITIES { - if let Some(entity) = server.entities.get_mut(client.state) { - while handle_event_default(client, entity).is_some() {} + if let Some(player) = server.entities.get_mut(client.state) { + while let Some(event) = client.next_event() { + event.handle_default(client, player); + } } } else { - while let Some(event) = client.pop_event() { - if let ClientEvent::SettingsChanged { view_distance, .. } = event { + while let Some(event) = client.next_event() { + if let ClientEvent::UpdateSettings { view_distance, .. } = event { client.set_view_distance(view_distance); } } diff --git a/src/chunk.rs b/src/chunk.rs index ed02c23..1a75ebf 100644 --- a/src/chunk.rs +++ b/src/chunk.rs @@ -10,6 +10,7 @@ use std::collections::hash_map::Entry; use std::collections::HashMap; use std::io::Write; use std::iter::FusedIterator; +use std::ops::{Deref, DerefMut}; use paletted_container::PalettedContainer; use rayon::iter::{IntoParallelRefIterator, IntoParallelRefMutIterator, ParallelIterator}; @@ -22,7 +23,7 @@ use valence_protocol::{BlockPos, BlockState, Encode, VarInt, VarLong}; use crate::biome::BiomeId; pub use crate::chunk_pos::ChunkPos; use crate::config::Config; -use crate::server::PlayPacketController; +use crate::server::PlayPacketSender; use crate::util::bits_needed; mod paletted_container; @@ -54,7 +55,7 @@ impl Chunks { /// /// **Note**: For the vanilla Minecraft client to see a chunk, all chunks /// adjacent to it must also be loaded. Clients should not be spawned within - /// unloaded chunks via [`spawn`](crate::client::Client::spawn). + /// unloaded chunks via [`respawn`](crate::client::Client::respawn). pub fn insert( &mut self, pos: impl Into, @@ -200,38 +201,46 @@ impl Chunks { } } - /// Sets the block state at an absolute block position in world space. + /// Sets the block state at an absolute block position in world space. The + /// previous block state at the position is returned. /// - /// If the position is inside of a chunk, then `true` is returned and the - /// block is set. Otherwise, `false` is returned and the function has no - /// effect. + /// If the given position is not inside of a loaded chunk, then a new chunk + /// is created at the position before the block is set. /// - /// **Note**: if you need to set a large number of blocks, it may be more - /// efficient write to the chunks directly with - /// [`Chunk::set_block_state`]. - pub fn set_block_state(&mut self, pos: impl Into, block: BlockState) -> bool { + /// If the position is completely out of bounds, then no new chunk is + /// created and [`BlockState::AIR`] is returned. + pub fn set_block_state(&mut self, pos: impl Into, block: BlockState) -> BlockState + where + C::ChunkState: Default, + { let pos = pos.into(); - let chunk_pos = ChunkPos::from(pos); - if let Some(chunk) = self.chunks.get_mut(&chunk_pos) { - if let Some(y) = pos - .y - .checked_sub(self.dimension_min_y) - .and_then(|y| y.try_into().ok()) - { - if y < chunk.height() { - chunk.set_block_state( - pos.x.rem_euclid(16) as usize, - y, - pos.z.rem_euclid(16) as usize, - block, - ); - return true; - } - } + let Some(y) = pos.y.checked_sub(self.dimension_min_y).and_then(|y| y.try_into().ok()) else { + return BlockState::AIR; + }; + + if y >= self.dimension_height as usize { + return BlockState::AIR; } - false + let chunk = match self.chunks.entry(ChunkPos::from(pos)) { + Entry::Occupied(oe) => oe.into_mut(), + Entry::Vacant(ve) => { + let dimension_section_count = (self.dimension_height / 16) as usize; + ve.insert(LoadedChunk::new( + UnloadedChunk::default(), + dimension_section_count, + Default::default(), + )) + } + }; + + chunk.set_block_state( + pos.x.rem_euclid(16) as usize, + y, + pos.z.rem_euclid(16) as usize, + block, + ) } pub(crate) fn update(&mut self) { @@ -472,6 +481,20 @@ pub struct LoadedChunk { created_this_tick: bool, } +impl Deref for LoadedChunk { + type Target = C::ChunkState; + + fn deref(&self) -> &Self::Target { + &self.state + } +} + +impl DerefMut for LoadedChunk { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.state + } +} + /// A 16x16x16 meter volume of blocks, biomes, and light in a chunk. #[derive(Clone)] struct ChunkSection { @@ -540,7 +563,7 @@ impl LoadedChunk { /// Queues the chunk data packet for this chunk with the given position. pub(crate) fn chunk_data_packet( &self, - ctrl: &mut PlayPacketController, + send: &mut PlayPacketSender, scratch: &mut Vec, pos: ChunkPos, biome_registry_len: usize, @@ -567,7 +590,7 @@ impl LoadedChunk { )?; } - ctrl.append_packet(&ChunkDataAndUpdateLight { + send.append_packet(&ChunkDataAndUpdateLight { chunk_x: pos.x, chunk_z: pos.z, heightmaps: compound! { @@ -590,7 +613,7 @@ impl LoadedChunk { &self, pos: ChunkPos, min_y: i32, - ctrl: &mut PlayPacketController, + send: &mut PlayPacketSender, ) -> anyhow::Result<()> { for (sect_y, sect) in self.sections.iter().enumerate() { if sect.modified_blocks_count == 1 { @@ -611,8 +634,8 @@ impl LoadedChunk { let global_y = sect_y as i32 * 16 + (idx / (16 * 16)) as i32 + min_y; let global_z = pos.z * 16 + (idx / 16 % 16) as i32; - ctrl.append_packet(&BlockUpdate { - location: BlockPos::new(global_x, global_y, global_z), + send.append_packet(&BlockUpdate { + position: BlockPos::new(global_x, global_y, global_z), block_id: VarInt(block.to_raw() as _), })?; } else if sect.modified_blocks_count > 1 { @@ -637,7 +660,7 @@ impl LoadedChunk { | (pos.z as i64 & 0x3fffff) << 20 | (sect_y as i64 + min_y.div_euclid(16) as i64) & 0xfffff; - ctrl.append_packet(&UpdateSectionBlocks { + send.append_packet(&UpdateSectionBlocks { chunk_section_position, invert_trust_edges: false, blocks, diff --git a/src/client.rs b/src/client.rs index ea1ca72..16f4aec 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,58 +1,54 @@ //! Connections to the server after logging in. -use std::collections::{HashSet, VecDeque}; +use std::collections::HashSet; use std::iter::FusedIterator; -use std::time::Duration; -use std::{cmp, mem}; +use std::net::IpAddr; +use std::num::Wrapping; +use std::ops::{Deref, DerefMut}; +use std::{array, mem}; use anyhow::{bail, Context}; pub use bitfield_struct::bitfield; -pub use event::*; +pub use event::ClientEvent; use rayon::iter::ParallelIterator; -use tracing::{error, info, warn}; +use tokio::sync::OwnedSemaphorePermit; +use tracing::{info, warn}; use uuid::Uuid; -use valence_protocol::packets::c2s::play::ClientCommand; use valence_protocol::packets::s2c::play::{ AcknowledgeBlockChange, ClearTitles, CombatDeath, CustomSoundEffect, DisconnectPlay, EntityAnimationS2c, EntityEvent, GameEvent, KeepAliveS2c, LoginPlayOwned, OpenScreen, PluginMessageS2c, RemoveEntities, ResourcePackS2c, RespawnOwned, SetActionBarText, - SetCenterChunk, SetContainerContent, SetDefaultSpawnPosition, SetEntityMetadata, - SetEntityVelocity, SetExperience, SetHeadRotation, SetHealth, SetRenderDistance, - SetSubtitleText, SetTitleAnimationTimes, SetTitleText, SynchronizePlayerPosition, - SystemChatMessage, TeleportEntity, UnloadChunk, UpdateAttributes, UpdateEntityPosition, - UpdateEntityPositionAndRotation, UpdateEntityRotation, UpdateTime, + SetCenterChunk, SetContainerContentEncode, SetContainerSlotEncode, SetDefaultSpawnPosition, + SetEntityMetadata, SetEntityVelocity, SetExperience, SetHeadRotation, SetHealth, + SetRenderDistance, SetSubtitleText, SetTitleAnimationTimes, SetTitleText, + SynchronizePlayerPosition, SystemChatMessage, TeleportEntity, UnloadChunk, UpdateAttributes, + UpdateEntityPosition, UpdateEntityPositionAndRotation, UpdateEntityRotation, UpdateTime, }; -use valence_protocol::packets::C2sPlayPacket; use valence_protocol::types::{ - Action, AttributeProperty, GameMode, GameStateChangeReason, SoundCategory, + AttributeProperty, DisplayedSkinParts, GameMode, GameStateChangeReason, SoundCategory, SyncPlayerPosLookFlags, }; use valence_protocol::{ - ident, types, BlockPos, ByteAngle, Encode, Ident, ItemStack, Packet, RawBytes, Text, Username, - VarInt, + BlockPos, ByteAngle, Encode, Ident, ItemStack, Packet, RawBytes, Text, Username, VarInt, }; use vek::Vec3; use crate::chunk_pos::ChunkPos; +use crate::client::event::next_event_fallible; use crate::config::Config; use crate::dimension::DimensionId; use crate::entity::data::Player; use crate::entity::{ self, velocity_to_packet_units, Entities, EntityId, EntityKind, StatusOrAnimation, }; -use crate::inventory::{ - Inventories, Inventory, InventoryDirtyable, InventoryError, InventoryId, PlayerInventory, - SlotId, WindowInventory, -}; +use crate::inventory::{Inventories, InventoryId}; use crate::player_list::{PlayerListId, PlayerLists}; use crate::player_textures::SignedPlayerTextures; -use crate::server::{NewClientData, PlayPacketController, SharedServer}; +use crate::server::{NewClientData, PlayPacketReceiver, PlayPacketSender, SharedServer}; use crate::slab_versioned::{Key, VersionedSlab}; use crate::util::{chunks_in_view_distance, is_chunk_in_view_distance}; use crate::world::{WorldId, Worlds}; -use crate::LIBRARY_NAMESPACE; -/// Contains the [`ClientEvent`] enum and related data types. mod event; /// A container for all [`Client`]s on a [`Server`](crate::server::Server). @@ -83,7 +79,7 @@ impl Clients { /// function has no effect. pub fn remove(&mut self, client: ClientId) -> Option { self.slab.remove(client.0).map(|c| { - info!(username = %c.username, uuid = %c.uuid, "removing client"); + info!(username = %c.username, uuid = %c.uuid, ip = %c.ip, "removing client"); c.state }) } @@ -94,7 +90,7 @@ impl Clients { pub fn retain(&mut self, mut f: impl FnMut(ClientId, &mut Client) -> bool) { self.slab.retain(|k, v| { if !f(ClientId(k), v) { - info!(username = %v.username, uuid = %v.uuid, "removing client"); + info!(username = %v.username, uuid = %v.uuid, ip = %v.ip, "removing client"); false } else { true @@ -182,7 +178,7 @@ impl ClientId { /// /// By default, clients have no influence over the worlds they reside in. They /// cannot break blocks, hurt entities, or see other clients. Interactions with -/// the server must be handled explicitly with [`Self::pop_event`]. +/// the server must be handled explicitly with [`Self::next_event`]. /// /// Additionally, clients possess [`Player`] entity data which is only visible /// to themselves. This can be accessed with [`Self::player`] and @@ -200,22 +196,24 @@ impl ClientId { pub struct Client { /// Custom state. pub state: C::ClientState, - /// Used for sending and receiving packets. - /// - /// Is `None` when the client is disconnected. - ctrl: Option, - uuid: Uuid, + send: Option, + recv: PlayPacketReceiver, + /// Ensures that we don't allow more connections to the server until the + /// client is dropped. + _permit: OwnedSemaphorePermit, username: Username, + uuid: Uuid, + ip: IpAddr, textures: Option, /// World client is currently in. Default value is **invalid** and must - /// be set by calling [`Client::spawn`]. + /// be set by calling [`Client::respawn`]. world: WorldId, player_list: Option, + /// Player list from the previous tick. old_player_list: Option, position: Vec3, + /// Position from the previous tick. old_position: Vec3, - /// Measured in m/s. - velocity: Vec3, /// Measured in degrees yaw: f32, /// Measured in degrees @@ -224,13 +222,10 @@ pub struct Client { /// 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 are ignored while this - /// is nonzero. + /// confirmation. Inbound client position packets should be ignored while + /// this is nonzero. pending_teleports: u32, - spawn_position: BlockPos, - spawn_position_yaw: f32, death_location: Option<(DimensionId, BlockPos)>, - events: VecDeque, /// The ID of the last keepalive sent. last_keepalive_id: u64, /// Entities that were visible to this client at the end of the last tick. @@ -238,106 +233,120 @@ pub struct Client { /// sent. loaded_entities: HashSet, loaded_chunks: HashSet, - new_game_mode: GameMode, - old_game_mode: GameMode, - settings: Option, + game_mode: GameMode, block_change_sequence: i32, - attack_speed: f64, - movement_speed: f64, - pub inventory: PlayerInventory, // TODO: make private or pub(crate) - pub open_inventory: Option, // TODO: make private or pub(crate) - bits: ClientBits, /// The data for the client's own player entity. player_data: Player, - entity_events: Vec, - /// The item currently being held by the client's cursor in an inventory - /// screen. Does not work for creative mode. - pub cursor_held_item: Option, // TODO: make private or pub(crate) - selected_hotbar_slot: SlotId, + /// The client's inventory slots. + slots: Box<[Option; 45]>, + /// Contains a set bit for each modified slot in `slots` made by the server + /// this tick. + modified_slots: u64, + /// Counts up as inventory modifications are made by the server. Used to + /// prevent desync. + inv_state_id: Wrapping, + /// The item currently held by the client's cursor in the inventory. + cursor_item: Option, + /// The currently open inventory. The client can close the screen, making + /// this [`InventoryId::NULL`]. + open_inventory: InventoryId, + /// The current window ID. Incremented when inventories are opened. + window_id: u8, + bits: ClientBits, } -#[bitfield(u16)] +#[bitfield(u8)] struct ClientBits { - spawn: bool, - flat: bool, - teleported_this_tick: bool, - /// If spawn_position or spawn_position_yaw were modified this tick. - modified_spawn_position: bool, + created_this_tick: bool, + respawn: bool, /// If the last sent keepalive got a response. got_keepalive: bool, hardcore: bool, - attack_speed_modified: bool, - movement_speed_modified: bool, - velocity_modified: bool, - created_this_tick: bool, - view_distance_modified: bool, - #[bits(5)] - _pad: u8, + flat: bool, + respawn_screen: bool, + cursor_item_modified: bool, + open_inventory_modified: bool, + //#[bits(1)] + //_pad: u8, +} + +impl Deref for Client { + type Target = C::ClientState; + + fn deref(&self) -> &Self::Target { + &self.state + } +} + +impl DerefMut for Client { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.state + } } impl Client { pub(crate) fn new( - ctrl: PlayPacketController, + send: PlayPacketSender, + recv: PlayPacketReceiver, + permit: OwnedSemaphorePermit, ncd: NewClientData, state: C::ClientState, ) -> Self { Self { state, - ctrl: Some(ctrl), - uuid: ncd.uuid, + send: Some(send), + recv, + _permit: permit, username: ncd.username, + uuid: ncd.uuid, + ip: ncd.ip, textures: ncd.textures, world: WorldId::default(), - old_player_list: None, player_list: None, + old_player_list: None, position: Vec3::default(), old_position: Vec3::default(), - velocity: Vec3::default(), yaw: 0.0, pitch: 0.0, view_distance: 2, teleport_id_counter: 0, pending_teleports: 0, - spawn_position: BlockPos::default(), - spawn_position_yaw: 0.0, death_location: None, - events: VecDeque::new(), last_keepalive_id: 0, loaded_entities: HashSet::new(), loaded_chunks: HashSet::new(), - new_game_mode: GameMode::Survival, - old_game_mode: GameMode::Survival, - settings: None, + game_mode: GameMode::Survival, block_change_sequence: 0, - attack_speed: 4.0, - movement_speed: 0.7, - inventory: PlayerInventory::new(), - open_inventory: None, + player_data: Player::new(), + slots: Box::new(array::from_fn(|_| None)), + modified_slots: 0, + inv_state_id: Wrapping(0), + cursor_item: None, + open_inventory: InventoryId::NULL, + window_id: 0, bits: ClientBits::new() - .with_modified_spawn_position(true) .with_got_keepalive(true) .with_created_this_tick(true), - player_data: Player::new(), - entity_events: Vec::new(), - cursor_held_item: None, - selected_hotbar_slot: PlayerInventory::HOTBAR_SLOTS.start, } } - /// Attempts to enqueue a play packet to be sent to this client. Has no - /// effect if the client is disconnected. + /// Attempts to enqueue a play packet to be sent to this client. + /// + /// If encoding the packet fails, the client is disconnected. Has no + /// effect if the client is already disconnected. pub fn queue_packet

(&mut self, pkt: &P) where P: Encode + Packet + ?Sized, { - if let Some(ctrl) = &mut self.ctrl { - if let Err(e) = ctrl.append_packet(pkt) { + if let Some(send) = &mut self.send { + if let Err(e) = send.append_packet(pkt) { warn!( username = %self.username, uuid = %self.uuid, + ip = %self.ip, "failed to queue packet: {e:#}" ); - self.ctrl = None; + self.send = None; } } } @@ -347,14 +356,19 @@ impl Client { self.bits.created_this_tick() } - /// Gets the client's UUID. + /// Gets the username of this client. + pub fn username(&self) -> Username<&str> { + self.username.as_str_username() + } + + /// Gets the UUID of this client. pub fn uuid(&self) -> Uuid { self.uuid } - /// Gets the username of this client. - pub fn username(&self) -> Username<&str> { - self.username.as_str_username() + /// Gets the IP address of this client. + pub fn ip(&self) -> IpAddr { + self.ip } /// Gets the player textures of this client. If the client does not have @@ -399,9 +413,11 @@ impl Client { /// /// The given [`WorldId`] must be valid. Otherwise, the client is /// disconnected. - pub fn spawn(&mut self, world: WorldId) { - self.world = world; - self.bits.set_spawn(true); + pub fn respawn(&mut self, world: WorldId) { + if self.world != world { + self.world = world; + self.bits.set_respawn(true); + } } /// Sends a system message to the player which is visible in the chat. The @@ -429,24 +445,31 @@ impl Client { /// Changes the position and rotation of this client in the world it is /// located in. /// - /// If you want to change the client's world, use [`Self::spawn`]. + /// If you want to change the client's world, use [`Self::respawn`]. pub fn teleport(&mut self, pos: impl Into>, yaw: f32, pitch: f32) { self.position = pos.into(); self.yaw = yaw; self.pitch = pitch; - self.bits.set_teleported_this_tick(true); - } + self.queue_packet(&SynchronizePlayerPosition { + position: self.position.into_array(), + yaw, + pitch, + flags: SyncPlayerPosLookFlags::new(), + teleport_id: VarInt(self.teleport_id_counter as i32), + dismount_vehicle: false, + }); - /// Gets the most recently set velocity of this client in m/s. - pub fn velocity(&self) -> Vec3 { - self.velocity + self.pending_teleports = self.pending_teleports.wrapping_add(1); + self.teleport_id_counter = self.teleport_id_counter.wrapping_add(1); } /// Sets the client's velocity in m/s. pub fn set_velocity(&mut self, velocity: impl Into>) { - self.velocity = velocity.into(); - self.bits.set_velocity_modified(true); + self.queue_packet(&SetEntityVelocity { + entity_id: VarInt(0), + velocity: velocity_to_packet_units(velocity.into()).into_array(), + }) } /// Gets this client's yaw. @@ -459,21 +482,13 @@ impl Client { self.pitch } - /// Gets the spawn position. The client will see `minecraft:compass` items - /// point at the returned position. - pub fn spawn_position(&self) -> BlockPos { - self.spawn_position - } - /// Sets the spawn position. The client will see `minecraft:compass` items /// point at the provided position. pub fn set_spawn_position(&mut self, pos: impl Into, yaw_degrees: f32) { - let pos = pos.into(); - if pos != self.spawn_position || yaw_degrees != self.spawn_position_yaw { - self.spawn_position = pos; - self.spawn_position_yaw = yaw_degrees; - self.bits.set_modified_spawn_position(true); - } + self.queue_packet(&SetDefaultSpawnPosition { + position: pos.into(), + angle: yaw_degrees, + }); } /// Gets the last death location of this client. The client will see @@ -500,12 +515,21 @@ impl Client { /// Gets the client's game mode. pub fn game_mode(&self) -> GameMode { - self.new_game_mode + self.game_mode } /// Sets the client's game mode. pub fn set_game_mode(&mut self, game_mode: GameMode) { - self.new_game_mode = game_mode; + if self.game_mode != game_mode { + self.game_mode = game_mode; + + if !self.created_this_tick() { + self.queue_packet(&GameEvent { + reason: GameStateChangeReason::ChangeGameMode, + value: game_mode as i32 as f32, + }); + } + } } /// Sets whether or not the client sees rain. @@ -561,16 +585,16 @@ impl Client { position: (pos.as_() * 8).into_array(), volume, pitch, - seed: 0, + seed: rand::random(), }); } /// Sets the title this client sees. /// /// A title is a large piece of text displayed in the center of the screen - /// which may also include a subtitle underneath it. The title - /// can be configured to fade in and out using the - /// [`SetTitleAnimationTimes`] struct. + /// which may also include a subtitle underneath it. The title can be + /// configured to fade in and out using the [`SetTitleAnimationTimes`] + /// struct. pub fn set_title( &mut self, title: impl Into, @@ -596,30 +620,28 @@ impl Client { self.queue_packet(&SetActionBarText(text.into())); } - /// Gets the attack cooldown speed. - pub fn attack_speed(&self) -> f64 { - self.attack_speed - } - /// Sets the attack cooldown speed. pub fn set_attack_speed(&mut self, speed: f64) { - if self.attack_speed != speed { - self.attack_speed = speed; - self.bits.set_attack_speed_modified(true); - } - } - - /// Gets the speed at which the client can run on the ground. - pub fn movement_speed(&self) -> f64 { - self.movement_speed + self.queue_packet(&UpdateAttributes { + entity_id: VarInt(0), + properties: vec![AttributeProperty { + key: Ident::new("generic.attack_speed").unwrap(), + value: speed, + modifiers: Vec::new(), + }], + }); } /// Sets the speed at which the client can run on the ground. pub fn set_movement_speed(&mut self, speed: f64) { - if self.movement_speed != speed { - self.movement_speed = speed; - self.bits.set_movement_speed_modified(true); - } + self.queue_packet(&UpdateAttributes { + entity_id: VarInt(0), + properties: vec![AttributeProperty { + key: Ident::new("generic.movement_speed").unwrap(), + value: speed, + modifiers: Vec::new(), + }], + }); } /// Removes the current title from the client's screen. @@ -663,7 +685,7 @@ impl Client { pub fn kill(&mut self, killer: Option, message: impl Into) { self.queue_packet(&CombatDeath { player_id: VarInt(0), - entity_id: killer.map_or(-1, |k| k.to_network_id()), + entity_id: killer.map_or(-1, |k| k.to_raw()), message: message.into(), }); } @@ -676,12 +698,44 @@ impl Client { }); } + pub fn has_respawn_screen(&self) -> bool { + self.bits.respawn_screen() + } + /// Sets whether respawn screen should be displayed after client's death. pub fn set_respawn_screen(&mut self, enable: bool) { - self.queue_packet(&GameEvent { - reason: GameStateChangeReason::EnableRespawnScreen, - value: if enable { 0.0 } else { 1.0 }, - }); + if self.bits.respawn_screen() != enable { + self.bits.set_respawn_screen(enable); + + if !self.created_this_tick() { + self.queue_packet(&GameEvent { + reason: GameStateChangeReason::EnableRespawnScreen, + value: if enable { 0.0 } else { 1.0 }, + }); + } + } + } + + pub fn skin_parts(&self) -> DisplayedSkinParts { + DisplayedSkinParts::new() + .with_cape(self.player_data.get_cape()) + .with_jacket(self.player_data.get_jacket()) + .with_left_sleeve(self.player_data.get_left_sleeve()) + .with_right_sleeve(self.player_data.get_right_sleeve()) + .with_left_pants_leg(self.player_data.get_left_pants_leg()) + .with_right_pants_leg(self.player_data.get_right_pants_leg()) + .with_hat(self.player_data.get_hat()) + } + + pub fn set_skin_parts(&mut self, parts: DisplayedSkinParts) { + self.player_data.set_cape(parts.cape()); + self.player_data.set_jacket(parts.jacket()); + self.player_data.set_left_sleeve(parts.left_sleeve()); + self.player_data.set_right_sleeve(parts.right_sleeve()); + self.player_data.set_left_pants_leg(parts.left_pants_leg()); + self.player_data + .set_right_pants_leg(parts.right_pants_leg()); + self.player_data.set_hat(parts.hat()); } /// Gets whether or not the client is connected to the server. @@ -690,31 +744,21 @@ impl Client { /// responsibility to remove disconnected clients from the [`Clients`] /// container. pub fn is_disconnected(&self) -> bool { - self.ctrl.is_none() + self.send.is_none() } - /// Returns an iterator over all pending client events in the order they - /// will be removed from the queue. - pub fn events( - &self, - ) -> impl DoubleEndedIterator + ExactSizeIterator + FusedIterator + Clone + '_ - { - self.events.iter() - } - - /// Removes a [`ClientEvent`] from the event queue. - /// - /// If there are no remaining events, `None` is returned. - /// - /// Any remaining client events are deleted at the end of the - /// current tick. - pub fn pop_event(&mut self) -> Option { - self.events.pop_front() - } - - /// Pushes an entity event to the queue. - pub fn push_entity_event(&mut self, event: entity::EntityEvent) { - self.entity_events.push(event); + /// Sends an entity event for the client's own player data. + pub fn send_entity_event(&mut self, event: entity::EntityEvent) { + match event.status_or_animation() { + StatusOrAnimation::Status(code) => self.queue_packet(&EntityEvent { + entity_id: 0, + entity_status: code, + }), + StatusOrAnimation::Animation(code) => self.queue_packet(&EntityAnimationS2c { + entity_id: VarInt(0), + animation: code, + }), + } } /// The current view distance of this client measured in chunks. The client @@ -734,7 +778,11 @@ impl Client { if self.view_distance != dist { self.view_distance = dist; - self.bits.set_view_distance_modified(true); + + if !self.created_this_tick() { + // Change the render distance fog. + self.queue_packet(&SetRenderDistance(VarInt(dist as i32))); + } } } @@ -766,13 +814,13 @@ impl Client { url: &str, hash: &str, forced: bool, - prompt_message: impl Into>, + prompt_message: Option, ) { self.queue_packet(&ResourcePackS2c { url, hash, forced, - prompt_message: prompt_message.into(), + prompt_message, }); } @@ -787,62 +835,21 @@ impl Client { }); } - /// Gets the client's current settings. - pub fn settings(&self) -> Option<&Settings> { - self.settings.as_ref() - } - - /// The slot that the client has selected in their hotbar. - pub fn held_item(&self) -> Option<&ItemStack> { - self.inventory.slot(self.selected_hotbar_slot) - } - - /// Consume items from the stack in the client's inventory that the client - /// is holding. - pub fn consume_held_item(&mut self, amount: impl Into) -> Result<(), InventoryError> { - self.inventory.consume(self.selected_hotbar_slot, amount) - } - - /// Makes the client open a window displaying the given inventory. - pub fn open_inventory( - &mut self, - inventories: &Inventories, - id: InventoryId, - window_title: impl Into, - ) { - if let Some(inv) = inventories.get(id) { - let window = WindowInventory::new(1, id); - self.queue_packet(&OpenScreen { - window_id: VarInt(window.window_id.into()), - window_type: inv.window_type, - window_title: window_title.into(), - }); - self.open_inventory = Some(window); - } - } - /// Disconnects this client from the server with the provided reason. This /// has no effect if the client is already disconnected. /// /// All future calls to [`Self::is_disconnected`] will return `true`. pub fn disconnect(&mut self, reason: impl Into) { - if self.ctrl.is_some() { - let txt = reason.into(); - info!("disconnecting client '{}': \"{txt}\"", self.username); - - self.queue_packet(&DisconnectPlay { reason: txt }); - - self.ctrl = None; - } + self.queue_packet(&DisconnectPlay { + reason: reason.into(), + }); + self.disconnect_abrupt(); } /// Like [`Self::disconnect`], but no reason for the disconnect is - /// displayed. - pub fn disconnect_no_reason(&mut self) { - if self.ctrl.is_some() { - info!("disconnecting client '{}'", self.username); - self.ctrl = None; - } + /// sent to the client. + pub fn disconnect_abrupt(&mut self) { + self.send = None; } /// Returns an immutable reference to the client's own [`Player`] data. @@ -857,314 +864,74 @@ impl Client { &mut self.player_data } - pub(crate) fn handle_serverbound_packets(&mut self, entities: &Entities) { - self.events.clear(); + pub fn slot(&self, idx: u16) -> Option<&ItemStack> { + self.slots + .get(idx as usize) + .expect("slot index out of range") + .as_ref() + } - if let Some(mut ctrl) = self.ctrl.take() { - if !ctrl.try_recv() { - return; - } + pub fn replace_slot( + &mut self, + idx: u16, + item: impl Into>, + ) -> Option { + assert!((idx as usize) < self.slots.len(), "slot index out of range"); - loop { - match ctrl.try_next_packet::() { - Ok(Some(pkt)) => { - let name = pkt.packet_name(); - if let Err(e) = self.handle_serverbound_packet(entities, pkt) { - error!( - "failed to handle {name} packet from client {}: {e:#}", - &self.username - ); - return; - } - } - Ok(None) => { - self.ctrl = Some(ctrl); - return; - } - Err(e) => { - error!( - "failed to read next serverbound packet from client {}: {e:#}", - &self.username - ); - return; - } - } + let new = item.into(); + let old = &mut self.slots[idx as usize]; + + if new != *old { + self.modified_slots |= 1 << idx; + } + + mem::replace(old, new) + } + + pub fn cursor_item(&self) -> Option<&ItemStack> { + self.cursor_item.as_ref() + } + + pub fn replace_cursor_item(&mut self, item: impl Into>) -> Option { + let new = item.into(); + if self.cursor_item != new { + todo!("set cursor item bit"); + } + + mem::replace(&mut self.cursor_item, new) + } + + pub fn open_inventory(&self) -> InventoryId { + self.open_inventory + } + + pub fn set_open_inventory(&mut self, id: InventoryId) { + if self.open_inventory != id { + self.bits.set_open_inventory_modified(true); + self.open_inventory = id; + } + } + + pub fn next_event(&mut self) -> Option { + match next_event_fallible(self) { + Ok(event) => event, + Err(e) => { + warn!( + username = %self.username, + uuid = %self.uuid, + ip = %self.ip, + "failed to get next event: {e:#}" + ); + self.send = None; + None } } } - fn handle_serverbound_packet( - &mut self, - entities: &Entities, - pkt: C2sPlayPacket, - ) -> anyhow::Result<()> { - match pkt { - C2sPlayPacket::ConfirmTeleport(p) => { - if self.pending_teleports == 0 { - bail!("unexpected teleport confirmation"); - } - - let got = p.teleport_id.0 as u32; - let expected = self - .teleport_id_counter - .wrapping_sub(self.pending_teleports); - - if got == expected { - self.pending_teleports -= 1; - } else { - bail!("unexpected teleport ID (expected {expected}, got {got}"); - } - } - C2sPlayPacket::QueryBlockEntityTag(_) => {} - C2sPlayPacket::ChangeDifficulty(_) => {} - C2sPlayPacket::MessageAcknowledgmentC2s(_) => {} - C2sPlayPacket::ChatCommand(_) => {} - C2sPlayPacket::ChatMessage(p) => self.events.push_back(ClientEvent::ChatMessage { - message: p.message.into(), - timestamp: Duration::from_millis(p.timestamp), - }), - C2sPlayPacket::ChatPreviewC2s(_) => {} - C2sPlayPacket::ClientCommand(p) => match p { - ClientCommand::PerformRespawn => { - self.events.push_back(ClientEvent::RespawnRequest); - } - ClientCommand::RequestStatus => (), - }, - C2sPlayPacket::ClientInformation(p) => { - self.events.push_back(ClientEvent::SettingsChanged { - locale: p.locale.into(), - view_distance: p.view_distance, - chat_mode: p.chat_mode, - chat_colors: p.chat_colors, - main_hand: p.main_hand, - displayed_skin_parts: p.displayed_skin_parts, - allow_server_listings: p.allow_server_listings, - }) - } - C2sPlayPacket::CommandSuggestionsRequest(_) => {} - C2sPlayPacket::ClickContainerButton(_) => {} - C2sPlayPacket::ClickContainer(p) => { - if p.slot_idx == -999 { - // client is trying to drop the currently held stack - let held = mem::replace(&mut self.cursor_held_item, None); - match held { - None => {} - Some(stack) => self.events.push_back(ClientEvent::DropItemStack { stack }), - } - } else { - self.cursor_held_item = p.carried_item.clone(); - self.events.push_back(ClientEvent::ClickContainer { - window_id: p.window_id, - state_id: p.state_id, - slot_id: p.slot_idx, - mode: p.mode, - slot_changes: p.slots, - carried_item: p.carried_item, - }); - } - } - C2sPlayPacket::CloseContainerC2s(c) => { - self.events.push_back(ClientEvent::CloseScreen { - window_id: c.window_id, - }) - } - C2sPlayPacket::EditBook(_) => {} - C2sPlayPacket::QueryEntityTag(_) => {} - C2sPlayPacket::Interact(p) => { - if let Some(id) = entities.get_with_network_id(p.entity_id.0) { - self.events.push_back(ClientEvent::InteractWithEntity { - id, - sneaking: p.sneaking, - interact: p.interact, - }); - } - } - C2sPlayPacket::JigsawGenerate(_) => {} - C2sPlayPacket::KeepAliveC2s(p) => { - let last_keepalive_id = self.last_keepalive_id; - if self.bits.got_keepalive() { - bail!("unexpected keepalive"); - } else if p.id != last_keepalive_id { - bail!( - "keepalive IDs don't match (expected {}, got {})", - last_keepalive_id, - p.id - ); - } else { - self.bits.set_got_keepalive(true); - } - } - C2sPlayPacket::LockDifficulty(_) => {} - C2sPlayPacket::SetPlayerPosition(p) => { - if self.pending_teleports == 0 { - self.position = p.position.into(); - - self.events.push_back(ClientEvent::MovePosition { - position: p.position.into(), - on_ground: p.on_ground, - }); - } - } - C2sPlayPacket::SetPlayerPositionAndRotation(p) => { - if self.pending_teleports == 0 { - self.position = p.position.into(); - self.yaw = p.yaw; - self.pitch = p.pitch; - - self.events.push_back(ClientEvent::MovePositionAndRotation { - position: p.position.into(), - yaw: p.yaw, - pitch: p.pitch, - on_ground: p.on_ground, - }); - } - } - C2sPlayPacket::SetPlayerRotation(p) => { - if self.pending_teleports == 0 { - self.yaw = p.yaw; - self.pitch = p.pitch; - - self.events.push_back(ClientEvent::MoveRotation { - yaw: p.yaw, - pitch: p.pitch, - on_ground: p.on_ground, - }); - } - } - C2sPlayPacket::SetPlayerOnGround(p) => { - if self.pending_teleports == 0 { - self.events - .push_back(ClientEvent::MoveOnGround { on_ground: p.0 }); - } - } - C2sPlayPacket::MoveVehicleC2s(p) => { - if self.pending_teleports == 0 { - self.position = p.position.into(); - self.yaw = p.yaw; - self.pitch = p.pitch; - - self.events.push_back(ClientEvent::MoveVehicle { - position: p.position.into(), - yaw: p.yaw, - pitch: p.pitch, - }); - } - } - C2sPlayPacket::PaddleBoat(p) => { - self.events.push_back(ClientEvent::SteerBoat { - left_paddle_turning: p.left_paddle_turning, - right_paddle_turning: p.right_paddle_turning, - }); - } - C2sPlayPacket::PickItem(_) => {} - C2sPlayPacket::PlaceRecipe(_) => {} - C2sPlayPacket::PlayerAbilitiesC2s(_) => {} - C2sPlayPacket::PlayerAction(p) => { - if p.sequence.0 != 0 { - self.block_change_sequence = cmp::max(p.sequence.0, self.block_change_sequence); - } - - self.events.push_back(match p.status { - types::DiggingStatus::StartedDigging => ClientEvent::Digging { - status: DiggingStatus::Start, - position: p.location, - face: p.face, - }, - types::DiggingStatus::CancelledDigging => ClientEvent::Digging { - status: DiggingStatus::Cancel, - position: p.location, - face: p.face, - }, - types::DiggingStatus::FinishedDigging => ClientEvent::Digging { - status: DiggingStatus::Finish, - position: p.location, - face: p.face, - }, - types::DiggingStatus::DropItemStack => return Ok(()), - types::DiggingStatus::DropItem => ClientEvent::DropItem, - types::DiggingStatus::ShootArrowOrFinishEating => return Ok(()), - types::DiggingStatus::SwapItemInHand => return Ok(()), - }); - } - C2sPlayPacket::PlayerCommand(c) => { - self.events.push_back(match c.action_id { - Action::StartSneaking => ClientEvent::StartSneaking, - Action::StopSneaking => ClientEvent::StopSneaking, - Action::LeaveBed => ClientEvent::LeaveBed, - Action::StartSprinting => ClientEvent::StartSprinting, - Action::StopSprinting => ClientEvent::StopSprinting, - Action::StartJumpWithHorse => ClientEvent::StartJumpWithHorse { - jump_boost: c.jump_boost.0 as u8, - }, - Action::StopJumpWithHorse => ClientEvent::StopJumpWithHorse, - Action::OpenHorseInventory => ClientEvent::OpenHorseInventory, - Action::StartFlyingWithElytra => ClientEvent::StartFlyingWithElytra, - }); - } - C2sPlayPacket::PlayerInput(_) => {} - C2sPlayPacket::PongPlay(_) => {} - C2sPlayPacket::ChangeRecipeBookSettings(_) => {} - C2sPlayPacket::SetSeenRecipe(_) => {} - C2sPlayPacket::RenameItem(_) => {} - C2sPlayPacket::ResourcePackC2s(p) => self - .events - .push_back(ClientEvent::ResourcePackStatusChanged(p)), - C2sPlayPacket::SeenAdvancements(_) => {} - C2sPlayPacket::SelectTrade(_) => {} - C2sPlayPacket::SetBeaconEffect(_) => {} - C2sPlayPacket::SetHeldItemC2s(e) => { - self.selected_hotbar_slot = - PlayerInventory::hotbar_to_slot(e.slot).unwrap_or(self.selected_hotbar_slot); - } - C2sPlayPacket::ProgramCommandBlock(_) => {} - C2sPlayPacket::ProgramCommandBlockMinecart(_) => {} - C2sPlayPacket::SetCreativeModeSlot(e) => { - if e.slot == -1 { - // The client is trying to drop a stack of items - match e.clicked_item { - None => bail!("creative client tried to drop a stack of nothing."), - Some(stack) => self.events.push_back(ClientEvent::DropItemStack { stack }), - } - } else { - self.events.push_back(ClientEvent::SetSlotCreative { - slot_id: e.slot, - slot: e.clicked_item, - }) - } - } - C2sPlayPacket::PluginMessageC2s(p) => { - self.events.push_back(ClientEvent::PluginMessageReceived { - channel: p.channel.to_owned_ident(), - data: p.data.0.to_vec(), - }); - } - C2sPlayPacket::ProgramJigsawBlock(_) => {} - C2sPlayPacket::ProgramStructureBlock(_) => {} - C2sPlayPacket::UpdateSign(_) => {} - C2sPlayPacket::SwingArm(p) => self.events.push_back(ClientEvent::ArmSwing(p.0)), - C2sPlayPacket::TeleportToEntity(_) => {} - C2sPlayPacket::UseItemOn(p) => { - if p.sequence.0 != 0 { - self.block_change_sequence = cmp::max(p.sequence.0, self.block_change_sequence); - } - - self.events.push_back(ClientEvent::InteractWithBlock { - hand: p.hand, - location: p.location, - face: p.face, - cursor_pos: p.cursor_pos.into(), - head_inside_block: p.head_inside_block, - sequence: p.sequence, - }) - } - C2sPlayPacket::UseItem(p) => { - if p.sequence.0 != 0 { - self.block_change_sequence = cmp::max(p.sequence.0, self.block_change_sequence); - } - } + pub(crate) fn prepare_c2s_packets(&mut self) { + if !self.recv.try_recv() { + self.disconnect_abrupt(); } - - Ok(()) } pub(crate) fn update( @@ -1173,22 +940,24 @@ impl Client { entities: &Entities, worlds: &Worlds, player_lists: &PlayerLists, - inventories: &Inventories, + inventories: &Inventories, ) { - if let Some(mut ctrl) = self.ctrl.take() { + if let Some(mut send) = self.send.take() { match self.update_fallible( - &mut ctrl, + &mut send, shared, entities, worlds, player_lists, inventories, ) { - Ok(()) => self.ctrl = Some(ctrl), + Ok(()) => self.send = Some(send), Err(e) => { - error!( + let _ = send.append_packet(&DisconnectPlay { reason: "".into() }); + warn!( username = %self.username, uuid = %self.uuid, + ip = %self.ip, "error updating client: {e:#}" ); } @@ -1203,12 +972,12 @@ impl Client { /// the error is reported. fn update_fallible( &mut self, - ctrl: &mut PlayPacketController, + send: &mut PlayPacketSender, shared: &SharedServer, entities: &Entities, worlds: &Worlds, player_lists: &PlayerLists, - inventories: &Inventories, + inventories: &Inventories, ) -> anyhow::Result<()> { let world = match worlds.get(self.world) { Some(world) => world, @@ -1217,36 +986,35 @@ impl Client { let current_tick = shared.current_tick(); - // Send the join game packet and other initial packets. We defer this until now - // so that the user can set the client's initial location, game mode, etc. + // Send the login (play) packet and other initial packets. We defer this until + // now so that the user can set the client's initial location, game + // mode, etc. if self.created_this_tick() { - self.bits.set_spawn(false); + self.bits.set_respawn(false); - let mut dimension_names: Vec<_> = shared + let dimension_names: Vec<_> = shared .dimensions() .map(|(id, _)| id.dimension_name()) .collect(); - dimension_names.push(ident!("{LIBRARY_NAMESPACE}:dummy_dimension")); - // The login packet is prepended so that it is sent before all the other // packets. Some packets don't work correctly when sent before the login packet, // which is why we're doing this. - ctrl.prepend_packet(&LoginPlayOwned { - entity_id: 0, // EntityId 0 is reserved for clients. + send.prepend_packet(&LoginPlayOwned { + entity_id: 0, // ID 0 is reserved for clients. is_hardcore: self.bits.hardcore(), - game_mode: self.new_game_mode, + game_mode: self.game_mode, previous_game_mode: -1, dimension_names, registry_codec: shared.registry_codec().clone(), dimension_type_name: world.meta.dimension().dimension_type_name(), dimension_name: world.meta.dimension().dimension_name(), hashed_seed: 10, - max_players: VarInt(0), + max_players: VarInt(0), // Unused view_distance: VarInt(self.view_distance() as i32), simulation_distance: VarInt(16), reduced_debug_info: false, - enable_respawn_screen: false, + enable_respawn_screen: self.bits.respawn_screen(), is_debug: false, is_flat: self.bits.flat(), last_death_location: self @@ -1255,19 +1023,20 @@ impl Client { })?; if let Some(id) = &self.player_list { - player_lists.get(id).send_initial_packets(ctrl)?; + player_lists.get(id).send_initial_packets(send)?; } - - self.teleport(self.position(), self.yaw(), self.pitch()); } else { - if self.bits.spawn() { - self.bits.set_spawn(false); - self.loaded_entities.clear(); + if self.bits.respawn() { + self.bits.set_respawn(false); + + // TODO: changing worlds didn't unload entities? + //self.loaded_entities.clear(); self.loaded_chunks.clear(); + /* // Client bug workaround: send the client to a dummy dimension first. // TODO: is there actually a bug? - ctrl.append_packet(&RespawnOwned { + send.append_packet(&RespawnOwned { dimension_type_name: DimensionId(0).dimension_type_name(), dimension_name: ident!("{LIBRARY_NAMESPACE}:dummy_dimension"), hashed_seed: 0, @@ -1278,8 +1047,9 @@ impl Client { copy_metadata: true, last_death_location: None, })?; + */ - ctrl.append_packet(&RespawnOwned { + send.append_packet(&RespawnOwned { dimension_type_name: world.meta.dimension().dimension_type_name(), dimension_name: world.meta.dimension().dimension_name(), hashed_seed: 0, @@ -1292,89 +1062,32 @@ impl Client { .death_location .map(|(id, pos)| (id.dimension_name(), pos)), })?; - - self.teleport(self.position(), self.yaw(), self.pitch()); - } - - // Update game mode - if self.old_game_mode != self.new_game_mode { - self.old_game_mode = self.new_game_mode; - ctrl.append_packet(&GameEvent { - reason: GameStateChangeReason::ChangeGameMode, - value: self.new_game_mode as i32 as f32, - })?; } // If the player list was changed... if self.old_player_list != self.player_list { // Delete existing entries from old player list. if let Some(id) = &self.old_player_list { - player_lists.get(id).queue_clear_packets(ctrl)?; + player_lists.get(id).queue_clear_packets(send)?; } // Get initial packets for new player list. if let Some(id) = &self.player_list { - player_lists.get(id).send_initial_packets(ctrl)?; + player_lists.get(id).send_initial_packets(send)?; } self.old_player_list = self.player_list.clone(); } else if let Some(id) = &self.player_list { - // Update current player list. - player_lists.get(id).send_update_packets(ctrl)?; - } - } - - // Set player attributes - if self.bits.attack_speed_modified() { - self.bits.set_attack_speed_modified(false); - - ctrl.append_packet(&UpdateAttributes { - entity_id: VarInt(0), - properties: vec![AttributeProperty { - key: Ident::new("generic.attack_speed").unwrap(), - value: self.attack_speed, - modifiers: Vec::new(), - }], - })?; - } - - if self.bits.movement_speed_modified() { - self.bits.set_movement_speed_modified(false); - - ctrl.append_packet(&UpdateAttributes { - entity_id: VarInt(0), - properties: vec![AttributeProperty { - key: Ident::new("generic.movement_speed").unwrap(), - value: self.movement_speed, - modifiers: Vec::new(), - }], - })?; - } - - // Update the players spawn position (compass position) - if self.bits.modified_spawn_position() { - self.bits.set_modified_spawn_position(false); - - ctrl.append_packet(&SetDefaultSpawnPosition { - location: self.spawn_position, - angle: self.spawn_position_yaw, - })?; - } - - // Update view distance fog on the client. - if self.bits.view_distance_modified() { - self.bits.set_view_distance_modified(false); - - if !self.created_this_tick() { - ctrl.append_packet(&SetRenderDistance(VarInt(self.view_distance() as i32)))?; + // Otherwise, update current player list. + player_lists.get(id).send_update_packets(send)?; } } // Check if it's time to send another keepalive. - if current_tick % (shared.tick_rate() * 8) == 0 { + if current_tick % (shared.tick_rate() * 10) == 0 { if self.bits.got_keepalive() { let id = rand::random(); - ctrl.append_packet(&KeepAliveS2c { id })?; + send.append_packet(&KeepAliveS2c { id })?; self.last_keepalive_id = id; self.bits.set_got_keepalive(false); } else { @@ -1387,7 +1100,7 @@ impl Client { // Send the update view position packet if the client changes the chunk they're // in. if ChunkPos::at(self.old_position.x, self.old_position.z) != center { - ctrl.append_packet(&SetCenterChunk { + send.append_packet(&SetCenterChunk { chunk_x: VarInt(center.x), chunk_z: VarInt(center.z), })?; @@ -1407,12 +1120,12 @@ impl Client { if is_chunk_in_view_distance(center, pos, self.view_distance + cache) && !chunk.created_this_tick() { - let _ = chunk.block_change_packets(pos, dimension.min_y, ctrl); + let _ = chunk.block_change_packets(pos, dimension.min_y, send); return true; } } - let _ = ctrl.append_packet(&UnloadChunk { + let _ = send.append_packet(&UnloadChunk { chunk_x: pos.x, chunk_z: pos.z, }); @@ -1428,7 +1141,7 @@ impl Client { for pos in chunks_in_view_distance(center, self.view_distance) { if let Some(chunk) = world.chunks.get(pos) { if self.loaded_chunks.insert(pos) { - chunk.chunk_data_packet(ctrl, &mut scratch, pos, biome_registry_len)?; + chunk.chunk_data_packet(send, &mut scratch, pos, biome_registry_len)?; } } } @@ -1436,49 +1149,13 @@ impl Client { // Acknowledge broken/placed blocks. if self.block_change_sequence != 0 { - ctrl.append_packet(&AcknowledgeBlockChange { + send.append_packet(&AcknowledgeBlockChange { sequence: VarInt(self.block_change_sequence), })?; self.block_change_sequence = 0; } - // Teleport the player. - // - // This is done after the chunks are loaded so that the "downloading terrain" - // screen is closed at the appropriate time. - if self.bits.teleported_this_tick() { - self.bits.set_teleported_this_tick(false); - - ctrl.append_packet(&SynchronizePlayerPosition { - position: self.position.into_array(), - yaw: self.yaw, - pitch: self.pitch, - flags: SyncPlayerPosLookFlags::new(), - teleport_id: VarInt(self.teleport_id_counter as i32), - dismount_vehicle: false, - })?; - - self.pending_teleports = self.pending_teleports.wrapping_add(1); - - if self.pending_teleports == 0 { - bail!("too many pending teleports"); - } - - self.teleport_id_counter = self.teleport_id_counter.wrapping_add(1); - } - - // Set velocity. Do this after teleporting since teleporting sets velocity to - // zero. - if self.bits.velocity_modified() { - self.bits.set_velocity_modified(false); - - ctrl.append_packet(&SetEntityVelocity { - entity_id: VarInt(0), - velocity: velocity_to_packet_units(self.velocity).into_array(), - })?; - } - let mut entities_to_unload = Vec::new(); // Update all entities that are visible and unload entities that are no @@ -1487,8 +1164,10 @@ impl Client { self.loaded_entities.retain(|&id| { if let Some(entity) = entities.get(id) { debug_assert!(entity.kind() != EntityKind::Marker); - if self.position.distance(entity.position()) <= self.view_distance as f64 * 16.0 { - let _ = entity.send_updated_tracked_data(ctrl, id); + if self.world == entity.world() + && self.position.distance(entity.position()) <= self.view_distance as f64 * 16.0 + { + let _ = entity.send_updated_tracked_data(send, id); let position_delta = entity.position() - entity.old_position(); let needs_teleport = position_delta.map(f64::abs).reduce_partial_max() >= 8.0; @@ -1498,8 +1177,8 @@ impl Client { && !needs_teleport && flags.yaw_or_pitch_modified() { - let _ = ctrl.append_packet(&UpdateEntityPositionAndRotation { - entity_id: VarInt(id.to_network_id()), + let _ = send.append_packet(&UpdateEntityPositionAndRotation { + entity_id: VarInt(id.to_raw()), delta: (position_delta * 4096.0).as_::().into_array(), yaw: ByteAngle::from_degrees(entity.yaw()), pitch: ByteAngle::from_degrees(entity.pitch()), @@ -1507,16 +1186,16 @@ impl Client { }); } else { if entity.position() != entity.old_position() && !needs_teleport { - let _ = ctrl.append_packet(&UpdateEntityPosition { - entity_id: VarInt(id.to_network_id()), + let _ = send.append_packet(&UpdateEntityPosition { + entity_id: VarInt(id.to_raw()), delta: (position_delta * 4096.0).as_::().into_array(), on_ground: entity.on_ground(), }); } if flags.yaw_or_pitch_modified() { - let _ = ctrl.append_packet(&UpdateEntityRotation { - entity_id: VarInt(id.to_network_id()), + let _ = send.append_packet(&UpdateEntityRotation { + entity_id: VarInt(id.to_raw()), yaw: ByteAngle::from_degrees(entity.yaw()), pitch: ByteAngle::from_degrees(entity.pitch()), on_ground: entity.on_ground(), @@ -1525,8 +1204,8 @@ impl Client { } if needs_teleport { - let _ = ctrl.append_packet(&TeleportEntity { - entity_id: VarInt(id.to_network_id()), + let _ = send.append_packet(&TeleportEntity { + entity_id: VarInt(id.to_raw()), position: entity.position().into_array(), yaw: ByteAngle::from_degrees(entity.yaw()), pitch: ByteAngle::from_degrees(entity.pitch()), @@ -1535,31 +1214,31 @@ impl Client { } if flags.velocity_modified() { - let _ = ctrl.append_packet(&SetEntityVelocity { - entity_id: VarInt(id.to_network_id()), + let _ = send.append_packet(&SetEntityVelocity { + entity_id: VarInt(id.to_raw()), velocity: velocity_to_packet_units(entity.velocity()).into_array(), }); } if flags.head_yaw_modified() { - let _ = ctrl.append_packet(&SetHeadRotation { - entity_id: VarInt(id.to_network_id()), + let _ = send.append_packet(&SetHeadRotation { + entity_id: VarInt(id.to_raw()), head_yaw: ByteAngle::from_degrees(entity.head_yaw()), }); } - let _ = send_entity_events(ctrl, id.to_network_id(), entity.events()); + let _ = send_entity_events(send, id.to_raw(), entity.events()); return true; } } - entities_to_unload.push(VarInt(id.to_network_id())); + entities_to_unload.push(VarInt(id.to_raw())); false }); if !entities_to_unload.is_empty() { - ctrl.append_packet(&RemoveEntities { + send.append_packet(&RemoveEntities { entity_ids: entities_to_unload, })?; } @@ -1571,7 +1250,7 @@ impl Client { if !data.is_empty() { data.push(0xff); - ctrl.append_packet(&SetEntityMetadata { + send.append_packet(&SetEntityMetadata { entity_id: VarInt(0), metadata: RawBytes(&data), })?; @@ -1580,6 +1259,8 @@ impl Client { // Spawn new entities within the view distance. let pos = self.position(); let view_dist = self.view_distance; + self.player_data.clear_modifications(); + if let Some(e) = world.spatial_index.query( |bb| bb.projected_point(pos).distance(pos) <= view_dist as f64 * 16.0, |id, _| { @@ -1587,30 +1268,19 @@ impl Client { .get(id) .expect("entity IDs in spatial index should be valid at this point"); - // Skip spawning players not in the player list because they would be invisible - // otherwise. - // TODO: this can be removed in 1.19.3 - if entity.kind() == EntityKind::Player { - if let Some(list_id) = &self.player_list { - player_lists.get(list_id).entry(entity.uuid())?; - } else { - return None; - } - } - if entity.kind() != EntityKind::Marker && entity.uuid() != self.uuid && self.loaded_entities.insert(id) { - if let Err(e) = entity.send_spawn_packets(id, ctrl) { + if let Err(e) = entity.send_spawn_packets(id, send) { return Some(e); } - if let Err(e) = entity.send_initial_tracked_data(ctrl, id) { + if let Err(e) = entity.send_initial_tracked_data(send, id) { return Some(e); } - if let Err(e) = send_entity_events(ctrl, id.to_network_id(), entity.events()) { + if let Err(e) = send_entity_events(send, id.to_raw(), entity.events()) { return Some(e); } } @@ -1621,76 +1291,104 @@ impl Client { return Err(e); } - send_entity_events(ctrl, 0, &self.entity_events)?; - self.entity_events.clear(); + // Update the client's own inventory. + if self.modified_slots != 0 { + if self.created_this_tick() + || self.modified_slots == u64::MAX && self.bits.cursor_item_modified() + { + // Update the whole inventory. + send.append_packet(&SetContainerContentEncode { + window_id: 0, + state_id: VarInt(self.inv_state_id.0), + slots: self.slots.as_slice(), + carried_item: &self.cursor_item, + })?; - self.player_data.clear_modifications(); - self.old_position = self.position; - self.bits.set_created_this_tick(false); + self.inv_state_id += 1; + self.bits.set_cursor_item_modified(false); + } else { + // Update only the slots that were modified. + for (i, slot) in self.slots.iter().enumerate() { + if (self.modified_slots >> i) & 1 == 1 { + send.append_packet(&SetContainerSlotEncode { + window_id: 0, + state_id: VarInt(self.inv_state_id.0), + slot_idx: i as i16, + slot_data: slot.as_ref(), + })?; - // Update the player's inventory - if self.inventory.is_dirty() { - ctrl.append_packet(&SetContainerContent { - window_id: 0, - state_id: VarInt(self.inventory.state_id), - slots: self - .inventory - .slots() - .into_iter() - // FIXME: cloning is necessary here to build the packet. - // However, it should be possible to avoid the clone if this packet - // could consume refs - .map(|s| s.cloned()) - .collect(), - carried_item: self.cursor_held_item.clone(), - })?; - self.inventory.state_id = self.inventory.state_id.wrapping_add(1); - self.inventory.mark_dirty(false); + self.inv_state_id += 1; + } + } + } + + self.modified_slots = 0; } - // Update the client's UI if they have an open inventory. - if let Some(window) = self.open_inventory.as_ref() { - // this client has an inventory open - let obj_inv_id = window.object_inventory; - if let Some(obj_inv) = inventories.get(obj_inv_id) { - if obj_inv.is_dirty() { - let window_id = window.window_id; - let slots = window.slots(obj_inv, &self.inventory) - .into_iter() - // FIXME: cloning is necessary here to build the packet. - // However, it should be possible to avoid the clone if this packet - // could consume refs - .map(|s| s.cloned()) - .collect(); - let carried_item = self.cursor_held_item.clone(); - ctrl.append_packet(&SetContainerContent { - window_id, - state_id: VarInt(1), - slots, - carried_item, - })?; - } + if self.bits.cursor_item_modified() { + self.bits.set_cursor_item_modified(false); + + send.append_packet(&SetContainerSlotEncode { + window_id: -1, + state_id: VarInt(self.inv_state_id.0), + slot_idx: -1, + slot_data: self.cursor_item.as_ref(), + })?; + + self.inv_state_id += 1; + } + + // Update the window the client has opened. + if self.bits.open_inventory_modified() { + // Open a new window. + self.bits.set_open_inventory_modified(false); + + if let Some(inv) = inventories.get(self.open_inventory) { + self.window_id = self.window_id % 100 + 1; + self.inv_state_id += 1; + + send.append_packet(&OpenScreen { + window_id: VarInt(self.window_id.into()), + window_type: VarInt(inv.kind() as i32), + window_title: inv.title().clone(), + })?; + + send.append_packet(&SetContainerContentEncode { + window_id: self.window_id, + state_id: VarInt(self.inv_state_id.0), + slots: inv.slot_slice(), + carried_item: &self.cursor_item, + })?; + } + } else { + // Update an already open window. + if let Some(inv) = inventories.get(self.open_inventory) { + inv.send_update(send, self.window_id, &mut self.inv_state_id)?; } } - ctrl.flush().context("failed to flush packet queue")?; + // TODO: send close screen packet under what circumstances? + + self.old_position = self.position; + + send.flush().context("failed to flush packet queue")?; Ok(()) } } fn send_entity_events( - ctrl: &mut PlayPacketController, + send: &mut PlayPacketSender, entity_id: i32, events: &[entity::EntityEvent], ) -> anyhow::Result<()> { for &event in events { match event.status_or_animation() { - StatusOrAnimation::Status(code) => ctrl.append_packet(&EntityEvent { + StatusOrAnimation::Status(code) => send.append_packet(&EntityEvent { entity_id, entity_status: code, })?, - StatusOrAnimation::Animation(code) => ctrl.append_packet(&EntityAnimationS2c { + StatusOrAnimation::Animation(code) => send.append_packet(&EntityAnimationS2c { entity_id: VarInt(entity_id), animation: code, })?, diff --git a/src/client/event.rs b/src/client/event.rs index 79c42e7..ef91389 100644 --- a/src/client/event.rs +++ b/src/client/event.rs @@ -1,47 +1,54 @@ -use std::time::Duration; +use std::cmp; +use anyhow::bail; +use uuid::Uuid; use valence_protocol::entity_meta::Pose; -use valence_protocol::packets::c2s::play::ResourcePackC2s; -use valence_protocol::types::{ - ChatMode, ClickContainerMode, DisplayedSkinParts, EntityInteraction, Hand, MainHand, +use valence_protocol::packets::c2s::play::{ + ClientCommand, PlayerAbilitiesC2s, ResourcePackC2s, SeenAdvancements, }; -use valence_protocol::{BlockFace, BlockPos, Ident, ItemStack, VarInt}; -use vek::Vec3; +use valence_protocol::packets::C2sPlayPacket; +use valence_protocol::types::{ + Action, ChatMode, ClickContainerMode, CommandBlockMode, Difficulty, DiggingStatus, + DisplayedSkinParts, EntityInteraction, Hand, MainHand, RecipeBookId, StructureBlockAction, + StructureBlockFlags, StructureBlockMirror, StructureBlockMode, StructureBlockRotation, +}; +use valence_protocol::{BlockFace, BlockPos, Ident, ItemStack, VarLong}; -use super::Client; +use crate::client::Client; use crate::config::Config; -use crate::entity::{Entity, EntityEvent, EntityId, TrackedData}; -use crate::inventory::{Inventory, InventoryDirtyable, SlotId}; +use crate::entity::{Entity, EntityEvent, TrackedData}; -/// Represents an action performed by a client. +/// A discrete action performed by a client. /// -/// Client events can be obtained from -/// [`pop_event`](super::Client::pop_event). +/// Client events are a more convenient representation of the data contained in +/// a [`C2sPlayPacket`]. /// -/// # Event Validation -/// -/// [`Client`](super::Client) makes no attempt to validate events against the -/// expected rules for players. Malicious clients can teleport through walls, -/// interact with distant entities, sneak and sprint backwards, break -/// bedrock in survival mode, etc. -/// -/// It is best to think of events from clients as _requests_ to interact with -/// the server. It is then your responsibility to decide if the request should -/// be honored. -#[derive(Debug)] +/// [`C2sPlayPacket`]: crate::protocol::packets::C2sPlayPacket +#[derive(Clone, Debug)] pub enum ClientEvent { - /// A regular message was sent to the chat. - ChatMessage { - /// The content of the message - message: String, - /// The time the message was sent. - timestamp: Duration, + QueryBlockEntity { + position: BlockPos, + transaction_id: i32, }, - /// Settings were changed. This is always sent once after joining by the - /// vanilla client. - SettingsChanged { + ChangeDifficulty(Difficulty), + MessageAcknowledgment { + last_seen: Vec<(Uuid, Box<[u8]>)>, + last_received: Option<(Uuid, Box<[u8]>)>, + }, + ChatCommand { + command: Box, + timestamp: u64, + }, + ChatMessage { + message: Box, + timestamp: u64, + }, + ChatPreview, + PerformRespawn, + RequestStats, + UpdateSettings { /// e.g. en_US - locale: String, + locale: Box, /// The client side render distance, in chunks. /// /// The value is always in `2..=32`. @@ -49,200 +56,635 @@ pub enum ClientEvent { chat_mode: ChatMode, /// `true` if the client has chat colors enabled, `false` otherwise. chat_colors: bool, - main_hand: MainHand, displayed_skin_parts: DisplayedSkinParts, + main_hand: MainHand, + enable_text_filtering: bool, allow_server_listings: bool, }, - MovePosition { - position: Vec3, - on_ground: bool, + CommandSuggestionsRequest { + transaction_id: i32, + text: Box, }, - MovePositionAndRotation { - position: Vec3, - yaw: f32, - pitch: f32, - on_ground: bool, + ClickContainerButton { + window_id: i8, + button_id: i8, }, - MoveRotation { - yaw: f32, - pitch: f32, - on_ground: bool, + ClickContainer { + window_id: u8, + state_id: i32, + slot_id: i16, + button: i8, + mode: ClickContainerMode, + slot_changes: Vec<(i16, Option)>, + carried_item: Option, }, - MoveOnGround { - on_ground: bool, + CloseContainer { + window_id: i8, }, - MoveVehicle { - position: Vec3, - yaw: f32, - pitch: f32, + PluginMessage { + channel: Ident>, + data: Box<[u8]>, }, - StartSneaking, - StopSneaking, - StartSprinting, - StopSprinting, - /// A jump while on a horse started. - StartJumpWithHorse { - /// The power of the horse jump. - jump_boost: u8, + EditBook { + slot: i32, + entries: Vec>, + title: Option>, + }, + QueryEntity { + transaction_id: i32, + entity_id: i32, }, - /// A jump while on a horse stopped. - StopJumpWithHorse, - /// The client left a bed. - LeaveBed, - /// The inventory was opened while on a horse. - OpenHorseInventory, - StartFlyingWithElytra, - ArmSwing(Hand), /// Left or right click interaction with an entity's hitbox. InteractWithEntity { - /// The ID of the entity being interacted with. - id: EntityId, + /// The raw ID of the entity being interacted with. + entity_id: i32, /// If the client was sneaking during the interaction. sneaking: bool, /// The kind of interaction that occurred. interact: EntityInteraction, }, - SteerBoat { + JigsawGenerate { + position: BlockPos, + levels: i32, + keep_jigsaws: bool, + }, + LockDifficulty(bool), + // TODO: combine movement events? + SetPlayerPosition { + position: [f64; 3], + on_ground: bool, + }, + SetPlayerPositionAndRotation { + position: [f64; 3], + yaw: f32, + pitch: f32, + on_ground: bool, + }, + SetPlayerRotation { + yaw: f32, + pitch: f32, + on_ground: bool, + }, + SetPlayerOnGround(bool), + MoveVehicle { + position: [f64; 3], + yaw: f32, + pitch: f32, + }, + StartSneaking, + StopSneaking, + LeaveBed, + StartSprinting, + StopSprinting, + StartJumpWithHorse { + /// The power of the horse jump in `0..=100`. + jump_boost: u8, + }, + /// A jump while on a horse stopped. + StopJumpWithHorse, + /// The inventory was opened while on a horse. + OpenHorseInventory, + StartFlyingWithElytra, + PaddleBoat { left_paddle_turning: bool, right_paddle_turning: bool, }, - Digging { - /// The kind of digging event this is. - status: DiggingStatus, - /// The position of the block being broken. - position: BlockPos, - /// The face of the block being broken. - face: BlockFace, + PickItem { + slot_to_use: i32, }, - InteractWithBlock { + PlaceRecipe { + window_id: i8, + recipe: Ident>, + make_all: bool, + }, + StopFlying, + StartFlying, + StartDigging { + position: BlockPos, + face: BlockFace, + sequence: i32, + }, + CancelDigging { + position: BlockPos, + face: BlockFace, + sequence: i32, + }, + FinishDigging { + position: BlockPos, + face: BlockFace, + sequence: i32, + }, + DropItem, + DropItemStack, + /// Eating food, pulling back bows, using buckets, etc. + UpdateHeldItemState, + SwapItemInHand, + PlayerInput { + sideways: f32, + forward: f32, + jump: bool, + unmount: bool, + }, + Pong { + id: i32, + }, + ChangeRecipeBookSettings { + book_id: RecipeBookId, + book_open: bool, + filter_active: bool, + }, + SetSeenRecipe { + recipe_id: Ident>, + }, + RenameItem { + name: Box, + }, + ResourcePackLoaded, + ResourcePackDeclined, + ResourcePackFailedDownload, + ResourcePackAccepted, + OpenAdvancementTab { + tab_id: Ident>, + }, + CloseAdvancementScreen, + SelectTrade { + slot: i32, + }, + SetBeaconEffect { + primary_effect: Option, + secondary_effect: Option, + }, + SetHeldItem { + slot: i16, + }, + ProgramCommandBlock { + position: BlockPos, + command: Box, + mode: CommandBlockMode, + track_output: bool, + conditional: bool, + automatic: bool, + }, + ProgramCommandBlockMinecart { + entity_id: i32, + command: Box, + track_output: bool, + }, + SetCreativeModeSlot { + slot: i16, + clicked_item: Option, + }, + ProgramJigsawBlock { + position: BlockPos, + name: Ident>, + target: Ident>, + pool: Ident>, + final_state: Box, + joint_type: Box, + }, + ProgramStructureBlock { + position: BlockPos, + action: StructureBlockAction, + mode: StructureBlockMode, + name: Box, + offset_xyz: [i8; 3], + size_xyz: [i8; 3], + mirror: StructureBlockMirror, + rotation: StructureBlockRotation, + metadata: Box, + integrity: f32, + seed: VarLong, + flags: StructureBlockFlags, + }, + UpdateSign { + position: BlockPos, + lines: [Box; 4], + }, + SwingArm(Hand), + TeleportToEntity { + target: Uuid, + }, + UseItemOnBlock { /// The hand that was used hand: Hand, /// The location of the block that was interacted with - location: BlockPos, + position: BlockPos, /// The face of the block that was clicked face: BlockFace, - /// The pos inside of the block that was clicked on - cursor_pos: Vec3, + /// The position inside of the block that was clicked on + cursor_pos: [f32; 3], /// Whether or not the player's head is inside a block head_inside_block: bool, - /// Sequence number - sequence: VarInt, + /// Sequence number for synchronization + sequence: i32, }, - PluginMessageReceived { - channel: Ident, - data: Vec, + UseItem { + hand: Hand, + sequence: i32, }, - ResourcePackStatusChanged(ResourcePackC2s), - /// The client closed a screen. This occurs when the client closes their - /// inventory, closes a chest inventory, etc. - CloseScreen { - window_id: u8, - }, - /// The client is attempting to drop 1 of the currently held item. - DropItem, - /// The client is attempting to drop a stack of items. - /// - /// If the client is in creative mode, the items come from the void, so it - /// is safe to trust the contents of this event. Otherwise, you may need to - /// do some validation to make sure items are actually coming from the - /// user's inventory. - DropItemStack { - // TODO: maybe we could add `from_slot_id` to make validation easier - stack: ItemStack, - }, - /// The client is in creative mode, and is trying to set it's inventory slot - /// to a value. - SetSlotCreative { - /// The slot number that the client is trying to set. - slot_id: SlotId, - /// The contents of the slot. - slot: Option, - }, - /// The client is in survival mode, and is trying to modify an inventory. - ClickContainer { - window_id: u8, - state_id: VarInt, - /// The slot that was clicked - slot_id: SlotId, - /// The type of click that the user performed - mode: ClickContainerMode, - /// A list of slot ids and what their contents should be set to. - /// - /// It's not safe to blindly trust the contents of this. Servers need to - /// validate it if they want to prevent item duping. - slot_changes: Vec<(SlotId, Option)>, - /// The item that is now being carried by the user's cursor - carried_item: Option, - }, - RespawnRequest, } -#[derive(Clone, PartialEq, Debug)] -pub struct Settings { - /// e.g. en_US - pub locale: String, - /// 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 main_hand: MainHand, - pub displayed_skin_parts: DisplayedSkinParts, - pub allow_server_listings: bool, -} - -#[derive(Copy, Clone, PartialEq, Eq, Debug)] -pub enum DiggingStatus { - /// The client started digging a block. - Start, - /// The client stopped digging a block before it was fully broken. - Cancel, - /// The client finished digging a block successfully. - Finish, -} - -/// Pops one event from the event queue of `client` and expresses the event in a -/// reasonable way using `entity`. For instance, movement events are expressed -/// by changing the entity's position to match the received position. Rotation -/// events rotate the entity. etc. -/// -/// This function'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 function is left unspecified and is subject to -/// change. -/// -/// The popped event is returned unmodified. `None` is returned if there are no -/// more events in `client`. -pub fn handle_event_default( +pub(super) fn next_event_fallible( client: &mut Client, - entity: &mut Entity, -) -> Option { - let event = client.pop_event()?; +) -> anyhow::Result> { + loop { + let Some(pkt) = client.recv.try_next_packet::()? else { + return Ok(None) + }; - match &event { - ClientEvent::ChatMessage { .. } => {} - ClientEvent::SettingsChanged { - view_distance, - main_hand, - displayed_skin_parts, - .. - } => { - client.set_view_distance(*view_distance); + return Ok(Some(match pkt { + C2sPlayPacket::ConfirmTeleport(p) => { + if client.pending_teleports == 0 { + bail!("unexpected teleport confirmation"); + } - let player = client.player_mut(); + let got = p.teleport_id.0 as u32; + let expected = client + .teleport_id_counter + .wrapping_sub(client.pending_teleports); - player.set_cape(displayed_skin_parts.cape()); - player.set_jacket(displayed_skin_parts.jacket()); - player.set_left_sleeve(displayed_skin_parts.left_sleeve()); - player.set_right_sleeve(displayed_skin_parts.right_sleeve()); - player.set_left_pants_leg(displayed_skin_parts.left_pants_leg()); - player.set_right_pants_leg(displayed_skin_parts.right_pants_leg()); - player.set_hat(displayed_skin_parts.hat()); - player.set_main_arm(*main_hand as u8); + if got == expected { + client.pending_teleports -= 1; + } else { + bail!("unexpected teleport ID (expected {expected}, got {got}"); + } + + continue; + } + C2sPlayPacket::QueryBlockEntityTag(p) => ClientEvent::QueryBlockEntity { + position: p.position, + transaction_id: p.transaction_id.0, + }, + C2sPlayPacket::ChangeDifficulty(p) => ClientEvent::ChangeDifficulty(p.0), + C2sPlayPacket::MessageAcknowledgmentC2s(p) => ClientEvent::MessageAcknowledgment { + last_seen: p + .0 + .last_seen + .into_iter() + .map(|entry| (entry.profile_id, entry.signature.into())) + .collect(), + last_received: p + .0 + .last_received + .map(|entry| (entry.profile_id, entry.signature.into())), + }, + C2sPlayPacket::ChatCommand(p) => ClientEvent::ChatCommand { + command: p.command.into(), + timestamp: p.timestamp, + }, + C2sPlayPacket::ChatMessage(p) => ClientEvent::ChatMessage { + message: p.message.into(), + timestamp: p.timestamp, + }, + C2sPlayPacket::ChatPreviewC2s(_) => ClientEvent::ChatPreview, + C2sPlayPacket::ClientCommand(p) => match p { + ClientCommand::PerformRespawn => ClientEvent::PerformRespawn, + ClientCommand::RequestStats => ClientEvent::RequestStats, + }, + C2sPlayPacket::ClientInformation(p) => ClientEvent::UpdateSettings { + 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_hand: p.main_hand, + enable_text_filtering: p.enable_text_filtering, + allow_server_listings: p.allow_server_listings, + }, + C2sPlayPacket::CommandSuggestionsRequest(p) => ClientEvent::CommandSuggestionsRequest { + transaction_id: p.transaction_id.0, + text: p.text.into(), + }, + C2sPlayPacket::ClickContainerButton(p) => ClientEvent::ClickContainerButton { + window_id: p.window_id, + button_id: p.button_id, + }, + C2sPlayPacket::ClickContainer(p) => { + // TODO: check that the slot modifications are legal. + // TODO: update cursor item. + + for (idx, item) in &p.slots { + // TODO: check bounds on indices. + client.slots[*idx as usize] = item.clone(); + } + + ClientEvent::ClickContainer { + 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::CloseContainerC2s(p) => ClientEvent::CloseContainer { + window_id: p.window_id, + }, + C2sPlayPacket::PluginMessageC2s(p) => ClientEvent::PluginMessage { + channel: p.channel.into(), + data: p.data.0.into(), + }, + C2sPlayPacket::EditBook(p) => ClientEvent::EditBook { + slot: p.slot.0, + entries: p.entries.into_iter().map(From::from).collect(), + title: p.title.map(From::from), + }, + C2sPlayPacket::QueryEntityTag(p) => ClientEvent::QueryEntity { + transaction_id: p.transaction_id.0, + entity_id: p.entity_id.0, + }, + C2sPlayPacket::Interact(p) => ClientEvent::InteractWithEntity { + entity_id: p.entity_id.0, + sneaking: p.sneaking, + interact: p.interact, + }, + C2sPlayPacket::JigsawGenerate(p) => ClientEvent::JigsawGenerate { + position: p.position, + levels: p.levels.0, + keep_jigsaws: p.keep_jigsaws, + }, + C2sPlayPacket::KeepAliveC2s(p) => { + if client.bits.got_keepalive() { + bail!("unexpected keepalive"); + } else if p.id != client.last_keepalive_id { + bail!( + "keepalive IDs don't match (expected {}, got {})", + client.last_keepalive_id, + p.id + ); + } else { + client.bits.set_got_keepalive(true); + } + + continue; + } + C2sPlayPacket::LockDifficulty(p) => ClientEvent::LockDifficulty(p.0), + C2sPlayPacket::SetPlayerPosition(p) => { + if client.pending_teleports != 0 { + continue; + } + + client.position = p.position.into(); + + ClientEvent::SetPlayerPosition { + position: p.position, + on_ground: p.on_ground, + } + } + C2sPlayPacket::SetPlayerPositionAndRotation(p) => { + if client.pending_teleports != 0 { + continue; + } + + client.position = p.position.into(); + client.yaw = p.yaw; + client.pitch = p.pitch; + + ClientEvent::SetPlayerPositionAndRotation { + position: p.position, + yaw: p.yaw, + pitch: p.pitch, + on_ground: p.on_ground, + } + } + C2sPlayPacket::SetPlayerRotation(p) => { + if client.pending_teleports != 0 { + continue; + } + + client.yaw = p.yaw; + client.pitch = p.pitch; + + ClientEvent::SetPlayerRotation { + yaw: p.yaw, + pitch: p.pitch, + on_ground: false, + } + } + C2sPlayPacket::SetPlayerOnGround(p) => { + if client.pending_teleports != 0 { + continue; + } + + ClientEvent::SetPlayerOnGround(p.0) + } + C2sPlayPacket::MoveVehicleC2s(p) => { + if client.pending_teleports != 0 { + continue; + } + + client.position = p.position.into(); + client.yaw = p.yaw; + client.pitch = p.pitch; + + ClientEvent::MoveVehicle { + position: p.position, + yaw: p.yaw, + pitch: p.pitch, + } + } + C2sPlayPacket::PlayerCommand(p) => match p.action_id { + Action::StartSneaking => ClientEvent::StartSneaking, + Action::StopSneaking => ClientEvent::StopSneaking, + Action::LeaveBed => ClientEvent::LeaveBed, + Action::StartSprinting => ClientEvent::StartSprinting, + Action::StopSprinting => ClientEvent::StopSprinting, + Action::StartJumpWithHorse => ClientEvent::StartJumpWithHorse { + jump_boost: p.jump_boost.0.clamp(0, 100) as u8, + }, + Action::StopJumpWithHorse => ClientEvent::StopJumpWithHorse, + Action::OpenHorseInventory => ClientEvent::OpenHorseInventory, + Action::StartFlyingWithElytra => ClientEvent::StartFlyingWithElytra, + }, + C2sPlayPacket::PaddleBoat(p) => ClientEvent::PaddleBoat { + left_paddle_turning: p.left_paddle_turning, + right_paddle_turning: p.right_paddle_turning, + }, + C2sPlayPacket::PickItem(p) => ClientEvent::PickItem { + slot_to_use: p.slot_to_use.0, + }, + C2sPlayPacket::PlaceRecipe(p) => ClientEvent::PlaceRecipe { + window_id: p.window_id, + recipe: p.recipe.into(), + make_all: p.make_all, + }, + C2sPlayPacket::PlayerAbilitiesC2s(p) => match p { + PlayerAbilitiesC2s::StopFlying => ClientEvent::StopFlying, + PlayerAbilitiesC2s::StartFlying => ClientEvent::StartFlying, + }, + C2sPlayPacket::PlayerAction(p) => { + if p.sequence.0 != 0 { + client.block_change_sequence = + cmp::max(p.sequence.0, client.block_change_sequence); + } + + match p.status { + DiggingStatus::StartedDigging => ClientEvent::StartDigging { + position: p.position, + face: p.face, + sequence: p.sequence.0, + }, + DiggingStatus::CancelledDigging => ClientEvent::CancelDigging { + position: p.position, + face: p.face, + sequence: p.sequence.0, + }, + DiggingStatus::FinishedDigging => ClientEvent::FinishDigging { + position: p.position, + face: p.face, + sequence: p.sequence.0, + }, + DiggingStatus::DropItemStack => ClientEvent::DropItemStack, + DiggingStatus::DropItem => ClientEvent::DropItem, + DiggingStatus::UpdateHeldItemState => ClientEvent::UpdateHeldItemState, + DiggingStatus::SwapItemInHand => ClientEvent::SwapItemInHand, + } + } + C2sPlayPacket::PlayerInput(p) => ClientEvent::PlayerInput { + sideways: p.sideways, + forward: p.forward, + jump: p.flags.jump(), + unmount: p.flags.unmount(), + }, + C2sPlayPacket::PongPlay(p) => ClientEvent::Pong { id: p.id }, + C2sPlayPacket::ChangeRecipeBookSettings(p) => ClientEvent::ChangeRecipeBookSettings { + book_id: p.book_id, + book_open: p.book_open, + filter_active: p.filter_active, + }, + C2sPlayPacket::SetSeenRecipe(p) => ClientEvent::SetSeenRecipe { + recipe_id: p.recipe_id.into(), + }, + C2sPlayPacket::RenameItem(p) => ClientEvent::RenameItem { + name: p.item_name.into(), + }, + C2sPlayPacket::ResourcePackC2s(p) => match p { + ResourcePackC2s::SuccessfullyLoaded => ClientEvent::ResourcePackLoaded, + ResourcePackC2s::Declined => ClientEvent::ResourcePackDeclined, + ResourcePackC2s::FailedDownload => ClientEvent::ResourcePackFailedDownload, + ResourcePackC2s::Accepted => ClientEvent::ResourcePackAccepted, + }, + C2sPlayPacket::SeenAdvancements(p) => match p { + SeenAdvancements::OpenedTab { tab_id } => ClientEvent::OpenAdvancementTab { + tab_id: tab_id.into(), + }, + SeenAdvancements::ClosedScreen => ClientEvent::CloseAdvancementScreen, + }, + C2sPlayPacket::SelectTrade(p) => ClientEvent::SelectTrade { + slot: p.selected_slot.0, + }, + C2sPlayPacket::SetBeaconEffect(p) => ClientEvent::SetBeaconEffect { + primary_effect: p.primary_effect.map(|i| i.0), + secondary_effect: p.secondary_effect.map(|i| i.0), + }, + C2sPlayPacket::SetHeldItemC2s(p) => ClientEvent::SetHeldItem { slot: p.slot }, + C2sPlayPacket::ProgramCommandBlock(p) => ClientEvent::ProgramCommandBlock { + 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::ProgramCommandBlockMinecart(p) => { + ClientEvent::ProgramCommandBlockMinecart { + entity_id: p.entity_id.0, + command: p.command.into(), + track_output: p.track_output, + } + } + C2sPlayPacket::SetCreativeModeSlot(p) => ClientEvent::SetCreativeModeSlot { + slot: p.slot, + clicked_item: p.clicked_item, + }, + C2sPlayPacket::ProgramJigsawBlock(p) => ClientEvent::ProgramJigsawBlock { + 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::ProgramStructureBlock(p) => ClientEvent::ProgramStructureBlock { + 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, + flags: p.flags, + }, + C2sPlayPacket::UpdateSign(p) => ClientEvent::UpdateSign { + position: p.position, + lines: p.lines.map(From::from), + }, + C2sPlayPacket::SwingArm(p) => ClientEvent::SwingArm(p.0), + C2sPlayPacket::TeleportToEntity(p) => { + ClientEvent::TeleportToEntity { target: p.target } + } + C2sPlayPacket::UseItemOn(p) => { + if p.sequence.0 != 0 { + client.block_change_sequence = + cmp::max(p.sequence.0, client.block_change_sequence); + } + + ClientEvent::UseItemOnBlock { + hand: p.hand, + position: p.position, + face: p.face, + cursor_pos: p.cursor_pos, + head_inside_block: p.head_inside_block, + sequence: p.sequence.0, + } + } + C2sPlayPacket::UseItem(p) => { + if p.sequence.0 != 0 { + client.block_change_sequence = + cmp::max(p.sequence.0, client.block_change_sequence); + } + + ClientEvent::UseItem { + hand: p.hand, + sequence: p.sequence.0, + } + } + })); + } +} + +impl ClientEvent { + /// Takes a client event, a client, and an entity representing the client + /// and expresses the event in a reasonable way. + /// + /// For instance, movement events are expressed by changing the entity's + /// position/rotation to match the received movement, crouching makes the + /// entity crouch, etc. + /// + /// This function'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 function is left unspecified and + /// is subject to change. + pub fn handle_default(&self, client: &mut Client, entity: &mut Entity) { + match self { + ClientEvent::RequestStats => { + // TODO: award empty statistics + } + ClientEvent::UpdateSettings { + view_distance, + displayed_skin_parts, + main_hand, + .. + } => { + client.set_view_distance(*view_distance); + + let player = client.player_mut(); - if let TrackedData::Player(player) = entity.data_mut() { player.set_cape(displayed_skin_parts.cape()); player.set_jacket(displayed_skin_parts.jacket()); player.set_left_sleeve(displayed_skin_parts.left_sleeve()); @@ -251,103 +693,89 @@ pub fn handle_event_default( player.set_right_pants_leg(displayed_skin_parts.right_pants_leg()); player.set_hat(displayed_skin_parts.hat()); player.set_main_arm(*main_hand as u8); - } - } - ClientEvent::MovePosition { - position, - on_ground, - } => { - entity.set_position(*position); - entity.set_on_ground(*on_ground); - } - ClientEvent::MovePositionAndRotation { - position, - yaw, - pitch, - on_ground, - } => { - entity.set_position(*position); - entity.set_yaw(*yaw); - entity.set_head_yaw(*yaw); - entity.set_pitch(*pitch); - entity.set_on_ground(*on_ground); - } - ClientEvent::MoveRotation { - yaw, - pitch, - on_ground, - } => { - entity.set_yaw(*yaw); - entity.set_head_yaw(*yaw); - entity.set_pitch(*pitch); - entity.set_on_ground(*on_ground); - } - ClientEvent::MoveOnGround { on_ground } => { - entity.set_on_ground(*on_ground); - } - ClientEvent::MoveVehicle { .. } => {} - ClientEvent::StartSneaking => { - if let TrackedData::Player(player) = entity.data_mut() { - if player.get_pose() == Pose::Standing { - player.set_pose(Pose::Sneaking); + + if let TrackedData::Player(player) = entity.data_mut() { + player.set_cape(displayed_skin_parts.cape()); + player.set_jacket(displayed_skin_parts.jacket()); + player.set_left_sleeve(displayed_skin_parts.left_sleeve()); + player.set_right_sleeve(displayed_skin_parts.right_sleeve()); + player.set_left_pants_leg(displayed_skin_parts.left_pants_leg()); + player.set_right_pants_leg(displayed_skin_parts.right_pants_leg()); + player.set_hat(displayed_skin_parts.hat()); + player.set_main_arm(*main_hand as u8); } } - } - ClientEvent::StopSneaking => { - if let TrackedData::Player(player) = entity.data_mut() { - if player.get_pose() == Pose::Sneaking { - player.set_pose(Pose::Standing); + ClientEvent::CommandSuggestionsRequest { .. } => {} + ClientEvent::SetPlayerPosition { + position, + on_ground, + } => { + entity.set_position(*position); + entity.set_on_ground(*on_ground); + } + ClientEvent::SetPlayerPositionAndRotation { + position, + yaw, + pitch, + on_ground, + } => { + entity.set_position(*position); + entity.set_yaw(*yaw); + entity.set_head_yaw(*yaw); + entity.set_pitch(*pitch); + entity.set_on_ground(*on_ground); + } + ClientEvent::SetPlayerRotation { + yaw, + pitch, + on_ground, + } => { + entity.set_yaw(*yaw); + entity.set_head_yaw(*yaw); + entity.set_pitch(*pitch); + entity.set_on_ground(*on_ground); + } + ClientEvent::SetPlayerOnGround(on_ground) => entity.set_on_ground(*on_ground), + ClientEvent::MoveVehicle { + position, + yaw, + pitch, + } => { + entity.set_position(*position); + entity.set_yaw(*yaw); + entity.set_pitch(*pitch); + } + ClientEvent::StartSneaking => { + if let TrackedData::Player(player) = entity.data_mut() { + if player.get_pose() == Pose::Standing { + player.set_pose(Pose::Sneaking); + } } } - } - ClientEvent::StartSprinting => { - if let TrackedData::Player(player) = entity.data_mut() { - player.set_sprinting(true); - } - } - ClientEvent::StopSprinting => { - if let TrackedData::Player(player) = entity.data_mut() { - player.set_sprinting(false); - } - } - ClientEvent::StartJumpWithHorse { .. } => {} - ClientEvent::StopJumpWithHorse => {} - ClientEvent::LeaveBed => {} - ClientEvent::OpenHorseInventory => {} - ClientEvent::StartFlyingWithElytra => {} - ClientEvent::ArmSwing(hand) => { - entity.push_event(match hand { - Hand::Main => EntityEvent::SwingMainHand, - Hand::Off => EntityEvent::SwingOffHand, - }); - } - ClientEvent::InteractWithEntity { .. } => {} - ClientEvent::SteerBoat { .. } => {} - ClientEvent::Digging { .. } => {} - ClientEvent::InteractWithBlock { .. } => {} - ClientEvent::PluginMessageReceived { .. } => {} - ClientEvent::ResourcePackStatusChanged(_) => {} - ClientEvent::CloseScreen { window_id } => { - if let Some(window) = &client.open_inventory { - if window.window_id == *window_id { - client.open_inventory = None; + ClientEvent::StopSneaking => { + if let TrackedData::Player(player) = entity.data_mut() { + if player.get_pose() == Pose::Sneaking { + player.set_pose(Pose::Standing); + } } } + ClientEvent::StartSprinting => { + if let TrackedData::Player(player) = entity.data_mut() { + player.set_sprinting(true); + } + } + ClientEvent::StopSprinting => { + if let TrackedData::Player(player) = entity.data_mut() { + player.set_sprinting(false); + } + } + ClientEvent::SwingArm(hand) => { + entity.push_event(match hand { + Hand::Main => EntityEvent::SwingMainHand, + Hand::Off => EntityEvent::SwingOffHand, + }); + } + _ => {} } - ClientEvent::DropItem => {} - ClientEvent::DropItemStack { .. } => {} - ClientEvent::SetSlotCreative { slot_id, slot } => { - let previous_dirty = client.inventory.is_dirty(); - client.inventory.set_slot(*slot_id, slot.clone()); - // HACK: we don't need to mark the inventory as dirty because the - // client already knows what the updated state of the inventory is. - client.inventory.mark_dirty(previous_dirty); - } - ClientEvent::ClickContainer { .. } => {} - ClientEvent::RespawnRequest => {} } - - entity.set_world(client.world()); - - Some(event) } diff --git a/src/config.rs b/src/config.rs index 3ea789a..3aa823d 100644 --- a/src/config.rs +++ b/src/config.rs @@ -40,6 +40,9 @@ pub trait Config: Sized + Send + Sync + 'static { /// Custom state to store with every /// [`PlayerList`](crate::player_list::PlayerList). type PlayerListState: Send + Sync; + /// Custom state to store with every + /// [`Inventory`](crate::inventory::Inventory). + type InventoryState: Send + Sync; /// Called once at startup to get the maximum number of simultaneous /// connections allowed to the server. This includes all @@ -305,7 +308,6 @@ pub trait Config: Sized + Send + Sync + 'static { } /// The result of the [`server_list_ping`](Config::server_list_ping) callback. -#[allow(clippy::large_enum_variant)] #[derive(Clone, Debug)] pub enum ServerListPing<'a> { /// Responds to the server list ping with the given information. @@ -331,6 +333,18 @@ pub enum ServerListPing<'a> { Ignore, } +/// Represents an individual entry in the player sample. +#[derive(Clone, Debug, Serialize)] +pub struct PlayerSampleEntry<'a> { + /// The name of the player. + /// + /// This string can contain + /// [legacy formatting codes](https://minecraft.fandom.com/wiki/Formatting_codes). + pub name: Cow<'a, str>, + /// The player UUID. + pub id: Uuid, +} + /// Describes how new connections to the server are handled. #[non_exhaustive] #[derive(Clone, PartialEq, Default)] @@ -386,26 +400,14 @@ pub enum ConnectionMode { }, } -/// Represents an individual entry in the player sample. -#[derive(Clone, Debug, Serialize)] -pub struct PlayerSampleEntry<'a> { - /// The name of the player. - /// - /// This string can contain - /// [legacy formatting codes](https://minecraft.fandom.com/wiki/Formatting_codes). - pub name: Cow<'a, str>, - /// The player UUID. - pub id: Uuid, -} - /// A minimal `Config` implementation for testing purposes. #[cfg(test)] -pub(crate) struct MockConfig { - _marker: std::marker::PhantomData<(S, Cl, E, W, Ch, P)>, +pub(crate) struct MockConfig { + _marker: std::marker::PhantomData<(S, Cl, E, W, Ch, P, I)>, } #[cfg(test)] -impl Config for MockConfig +impl Config for MockConfig where S: Send + Sync + 'static, Cl: Default + Send + Sync + 'static, @@ -413,6 +415,7 @@ where W: Send + Sync + 'static, Ch: Send + Sync + 'static, P: Send + Sync + 'static, + I: Send + Sync + 'static, { type ServerState = S; type ClientState = Cl; @@ -420,4 +423,5 @@ where type WorldState = W; type ChunkState = Ch; type PlayerListState = P; + type InventoryState = I; } diff --git a/src/entity.rs b/src/entity.rs index 032182c..7ef4276 100644 --- a/src/entity.rs +++ b/src/entity.rs @@ -4,6 +4,7 @@ use std::collections::hash_map::Entry; use std::collections::HashMap; use std::iter::FusedIterator; use std::num::NonZeroU32; +use std::ops::{Deref, DerefMut}; use bitfield_struct::bitfield; pub use data::{EntityKind, TrackedData}; @@ -17,7 +18,7 @@ use valence_protocol::{ByteAngle, RawBytes, VarInt}; use vek::{Aabb, Vec3}; use crate::config::Config; -use crate::server::PlayPacketController; +use crate::server::PlayPacketSender; use crate::slab_versioned::{Key, VersionedSlab}; use crate::util::aabb_from_bottom_and_size; use crate::world::WorldId; @@ -40,7 +41,7 @@ include!(concat!(env!("OUT_DIR"), "/entity_event.rs")); pub struct Entities { slab: VersionedSlab>, uuid_to_entity: HashMap, - network_id_to_entity: HashMap, + raw_id_to_entity: HashMap, } impl Entities { @@ -48,7 +49,7 @@ impl Entities { Self { slab: VersionedSlab::new(), uuid_to_entity: HashMap::new(), - network_id_to_entity: HashMap::new(), + raw_id_to_entity: HashMap::new(), } } @@ -93,7 +94,7 @@ impl Entities { }); // TODO check for overflowing version? - self.network_id_to_entity.insert(k.version(), k.index()); + self.raw_id_to_entity.insert(k.version(), k.index()); ve.insert(EntityId(k)); @@ -113,7 +114,7 @@ impl Entities { .remove(&e.uuid) .expect("UUID should have been in UUID map"); - self.network_id_to_entity + self.raw_id_to_entity .remove(&entity.0.version()) .expect("network ID should have been in the network ID map"); @@ -133,7 +134,7 @@ impl Entities { .remove(&v.uuid) .expect("UUID should have been in UUID map"); - self.network_id_to_entity + self.raw_id_to_entity .remove(&k.version()) .expect("network ID should have been in the network ID map"); @@ -174,10 +175,22 @@ impl Entities { self.slab.get_mut(entity.0) } - pub(crate) fn get_with_network_id(&self, network_id: i32) -> Option { - let version = NonZeroU32::new(network_id as u32)?; - let index = *self.network_id_to_entity.get(&version)?; - Some(EntityId(Key::new(index, version))) + pub fn get_with_raw_id(&self, raw_id: i32) -> Option<(EntityId, &Entity)> { + let version = NonZeroU32::new(raw_id as u32)?; + let index = *self.raw_id_to_entity.get(&version)?; + + let id = EntityId(Key::new(index, version)); + let entity = self.get(id)?; + Some((id, entity)) + } + + pub fn get_with_raw_id_mut(&mut self, raw_id: i32) -> Option<(EntityId, &mut Entity)> { + let version = NonZeroU32::new(raw_id as u32)?; + let index = *self.raw_id_to_entity.get(&version)?; + + let id = EntityId(Key::new(index, version)); + let entity = self.get_mut(id)?; + Some((id, entity)) } /// Returns an iterator over all entities on the server in an unspecified @@ -239,7 +252,7 @@ impl EntityId { /// The value of the default entity ID which is always invalid. pub const NULL: Self = Self(Key::NULL); - pub fn to_network_id(self) -> i32 { + pub fn to_raw(self) -> i32 { self.0.version().get() as i32 } } @@ -279,6 +292,20 @@ pub(crate) struct EntityBits { _pad: u8, } +impl Deref for Entity { + type Target = C::EntityState; + + fn deref(&self) -> &Self::Target { + &self.state + } +} + +impl DerefMut for Entity { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.state + } +} + impl Entity { pub(crate) fn bits(&self) -> EntityBits { self.bits @@ -709,13 +736,13 @@ impl Entity { /// been spawned. pub(crate) fn send_initial_tracked_data( &self, - ctrl: &mut PlayPacketController, + send: &mut PlayPacketSender, this_id: EntityId, ) -> anyhow::Result<()> { // TODO: cache metadata buffer? if let Some(metadata) = self.variants.initial_tracked_data() { - ctrl.append_packet(&SetEntityMetadata { - entity_id: VarInt(this_id.to_network_id()), + send.append_packet(&SetEntityMetadata { + entity_id: VarInt(this_id.to_raw()), metadata: RawBytes(&metadata), })?; } @@ -727,13 +754,13 @@ impl Entity { /// modified. pub(crate) fn send_updated_tracked_data( &self, - ctrl: &mut PlayPacketController, + send: &mut PlayPacketSender, this_id: EntityId, ) -> anyhow::Result<()> { // TODO: cache metadata buffer? if let Some(metadata) = self.variants.updated_tracked_data() { - ctrl.append_packet(&SetEntityMetadata { - entity_id: VarInt(this_id.to_network_id()), + send.append_packet(&SetEntityMetadata { + entity_id: VarInt(this_id.to_raw()), metadata: RawBytes(&metadata), })?; } @@ -745,10 +772,10 @@ impl Entity { pub(crate) fn send_spawn_packets( &self, this_id: EntityId, - ctrl: &mut PlayPacketController, + send: &mut PlayPacketSender, ) -> anyhow::Result<()> { let with_object_data = |data| SpawnEntity { - entity_id: VarInt(this_id.to_network_id()), + entity_id: VarInt(this_id.to_raw()), object_uuid: self.uuid, kind: VarInt(self.kind() as i32), position: self.new_position.into_array(), @@ -761,14 +788,14 @@ impl Entity { match &self.variants { TrackedData::Marker(_) => {} - TrackedData::ExperienceOrb(_) => ctrl.append_packet(&SpawnExperienceOrb { - entity_id: VarInt(this_id.to_network_id()), + TrackedData::ExperienceOrb(_) => send.append_packet(&SpawnExperienceOrb { + entity_id: VarInt(this_id.to_raw()), position: self.new_position.into_array(), count: 0, // TODO })?, TrackedData::Player(_) => { - ctrl.append_packet(&SpawnPlayer { - entity_id: VarInt(this_id.to_network_id()), + send.append_packet(&SpawnPlayer { + entity_id: VarInt(this_id.to_raw()), player_uuid: self.uuid, position: self.new_position.into_array(), yaw: ByteAngle::from_degrees(self.yaw), @@ -776,17 +803,17 @@ impl Entity { })?; // Player spawn packet doesn't include head yaw for some reason. - ctrl.append_packet(&SetHeadRotation { - entity_id: VarInt(this_id.to_network_id()), + send.append_packet(&SetHeadRotation { + entity_id: VarInt(this_id.to_raw()), head_yaw: ByteAngle::from_degrees(self.head_yaw), })?; } - TrackedData::ItemFrame(e) => ctrl.append_packet(&with_object_data(e.get_rotation()))?, + TrackedData::ItemFrame(e) => send.append_packet(&with_object_data(e.get_rotation()))?, TrackedData::GlowItemFrame(e) => { - ctrl.append_packet(&with_object_data(e.get_rotation()))? + send.append_packet(&with_object_data(e.get_rotation()))? } - TrackedData::Painting(_) => ctrl.append_packet(&with_object_data( + TrackedData::Painting(_) => send.append_packet(&with_object_data( match ((self.yaw + 45.0).rem_euclid(360.0) / 90.0) as u8 { 0 => 3, 1 => 4, @@ -795,14 +822,14 @@ impl Entity { }, ))?, // TODO: set block state ID for falling block. - TrackedData::FallingBlock(_) => ctrl.append_packet(&with_object_data(1))?, + TrackedData::FallingBlock(_) => send.append_packet(&with_object_data(1))?, TrackedData::FishingBobber(e) => { - ctrl.append_packet(&with_object_data(e.get_hook_entity_id()))? + send.append_packet(&with_object_data(e.get_hook_entity_id()))? } TrackedData::Warden(e) => { - ctrl.append_packet(&with_object_data((e.get_pose() == Pose::Emerging).into()))? + send.append_packet(&with_object_data((e.get_pose() == Pose::Emerging).into()))? } - _ => ctrl.append_packet(&with_object_data(0))?, + _ => send.append_packet(&with_object_data(0))?, } Ok(()) @@ -828,17 +855,17 @@ mod tests { #[test] fn entities_has_valid_new_state() { let mut entities: Entities = Entities::new(); - let network_id: i32 = 8675309; + let raw_id: i32 = 8675309; let entity_id = EntityId(Key::new( 202298, - NonZeroU32::new(network_id as u32).expect("Value given should never be zero!"), + NonZeroU32::new(raw_id as u32).expect("value given should never be zero!"), )); let uuid = Uuid::from_bytes([2; 16]); assert!(entities.is_empty()); assert!(entities.get(entity_id).is_none()); assert!(entities.get_mut(entity_id).is_none()); assert!(entities.get_with_uuid(uuid).is_none()); - assert!(entities.get_with_network_id(network_id).is_none()); + assert!(entities.get_with_raw_id(raw_id).is_none()); } #[test] @@ -850,7 +877,7 @@ mod tests { assert_eq!(entities.get(player_id).unwrap().state, 1); let mut_player_entity = entities .get_mut(player_id) - .expect("Failed to get mutable reference"); + .expect("failed to get mutable reference"); mut_player_entity.state = 100; assert_eq!(entities.get(player_id).unwrap().state, 100); assert_eq!(entities.len(), 1); @@ -863,17 +890,17 @@ mod tests { assert!(entities.is_empty()); let (zombie_id, zombie_entity) = entities .insert_with_uuid(EntityKind::Zombie, uuid, 1) - .expect("Unexpected Uuid collision when inserting to an empty collection"); + .expect("unexpected Uuid collision when inserting to an empty collection"); assert_eq!(zombie_entity.state, 1); let maybe_zombie = entities .get_with_uuid(uuid) - .expect("Uuid lookup failed on item already added to this collection"); + .expect("UUID lookup failed on item already added to this collection"); assert_eq!(zombie_id, maybe_zombie); assert_eq!(entities.len(), 1); } #[test] - fn entities_can_be_set_and_get_with_network_id() { + fn entities_can_be_set_and_get_with_raw_id() { let mut entities: Entities = Entities::new(); assert!(entities.is_empty()); let (boat_id, boat_entity) = entities.insert(EntityKind::Boat, 12); @@ -881,18 +908,20 @@ mod tests { let (cat_id, cat_entity) = entities.insert(EntityKind::Cat, 75); assert_eq!(cat_entity.state, 75); let maybe_boat_id = entities - .get_with_network_id(boat_id.0.version.get() as i32) - .expect("Network id lookup failed on item already added to this collection"); + .get_with_raw_id(boat_id.0.version.get() as i32) + .expect("raw id lookup failed on item already added to this collection") + .0; let maybe_boat = entities .get(maybe_boat_id) - .expect("Failed to look up item already added to collection"); + .expect("failed to look up item already added to collection"); assert_eq!(maybe_boat.state, 12); let maybe_cat_id = entities - .get_with_network_id(cat_id.0.version.get() as i32) - .expect("Network id lookup failed on item already added to this collection"); + .get_with_raw_id(cat_id.0.version.get() as i32) + .expect("raw id lookup failed on item already added to this collection") + .0; let maybe_cat = entities .get(maybe_cat_id) - .expect("Failed to look up item already added to collection"); + .expect("failed to look up item already added to collection"); assert_eq!(maybe_cat.state, 75); assert_eq!(entities.len(), 2); } @@ -904,7 +933,7 @@ mod tests { let (player_id, _) = entities.insert(EntityKind::Player, 1); let player_state = entities .remove(player_id) - .expect("Failed to remove an item from the collection"); + .expect("failed to remove an item from the collection"); assert_eq!(player_state, 1); } diff --git a/src/inventory.rs b/src/inventory.rs index 24123a4..28744e4 100644 --- a/src/inventory.rs +++ b/src/inventory.rs @@ -1,305 +1,183 @@ -use std::ops::Range; +use std::iter::FusedIterator; +use std::mem; +use std::num::Wrapping; +use std::ops::{Deref, DerefMut}; -use thiserror::Error; -use valence_protocol::{ItemStack, VarInt}; +use valence_protocol::packets::s2c::play::SetContainerSlotEncode; +use valence_protocol::{InventoryKind, ItemStack, Text, VarInt}; +use crate::config::Config; +use crate::server::PlayPacketSender; use crate::slab_versioned::{Key, VersionedSlab}; -pub type SlotId = i16; - -pub trait Inventory { - fn slot(&self, slot_id: SlotId) -> Option<&ItemStack>; - /// Sets the slot to the desired contents. Returns the previous contents of - /// the slot. - fn set_slot(&mut self, slot_id: SlotId, slot: Option) -> Option; - fn slot_range(&self) -> Range; - - fn slot_count(&self) -> usize { - self.slot_range().count() - } - - // TODO: `entry()` style api - - fn slots(&self) -> Vec> { - (0..self.slot_count()) - .map(|s| self.slot(s as SlotId)) - .collect() - } - - /// Decreases the count for stack in the slot by amount. If there is not - /// enough items in the stack to perform the operation, then it will fail. - /// - /// Returns `Ok` if the stack had enough items, and the operation was - /// carried out. Otherwise, it returns `Err` if `amount > stack.count()`, - /// and no changes were made to the inventory. - #[allow(clippy::unnecessary_unwrap)] - fn consume(&mut self, slot_id: SlotId, amount: impl Into) -> Result<(), InventoryError> { - let amount: u8 = amount.into(); - let slot = self.slot(slot_id).cloned(); - if slot.is_some() { - // Intentionally not using `if let` so stack can be moved out of the slot as mut - // to avoid another clone later. - let mut stack = slot.unwrap(); - if amount > stack.count() { - return Err(InventoryError); - } - let slot = if amount == stack.count() { - None - } else { - stack.set_count(stack.count() - amount); - Some(stack) - }; - - self.set_slot(slot_id, slot); - } - Ok(()) - } +pub struct Inventories { + slab: VersionedSlab>, } -pub(crate) trait InventoryDirtyable { - fn mark_dirty(&mut self, dirty: bool); - fn is_dirty(&self) -> bool; -} - -/// Represents a player's Inventory. -#[derive(Debug, Clone)] -pub struct PlayerInventory { - pub(crate) slots: Box<[Option; 46]>, - dirty: bool, - pub(crate) state_id: i32, -} - -impl PlayerInventory { - /// General slots are the slots that can hold all items, including the - /// hotbar, excluding offhand. These slots are shown when the player is - /// looking at another inventory. - pub const GENERAL_SLOTS: Range = 9..45; - pub const HOTBAR_SLOTS: Range = 36..45; - - pub fn hotbar_to_slot(hotbar_slot: i16) -> Option { - if !(0..=8).contains(&hotbar_slot) { - return None; - } - - Some(Self::HOTBAR_SLOTS.start + hotbar_slot) - } - - pub(crate) fn new() -> Self { - Self { - // Can't do the shorthand because Option is not Copy. - slots: Box::new(std::array::from_fn(|_| None)), - dirty: true, - state_id: Default::default(), - } - } -} - -impl Inventory for PlayerInventory { - fn slot(&self, slot_id: SlotId) -> Option<&ItemStack> { - if !self.slot_range().contains(&slot_id) { - return None; - } - self.slots[slot_id as usize].as_ref() - } - - fn set_slot(&mut self, slot_id: SlotId, slot: Option) -> Option { - if !self.slot_range().contains(&slot_id) { - return None; - } - self.mark_dirty(true); - std::mem::replace(&mut self.slots[slot_id as usize], slot) - } - - fn slot_range(&self) -> Range { - 0..(self.slots.len() as SlotId) - } -} - -impl InventoryDirtyable for PlayerInventory { - fn mark_dirty(&mut self, dirty: bool) { - self.dirty = dirty - } - - fn is_dirty(&self) -> bool { - self.dirty - } -} - -#[derive(Debug, Clone)] -pub struct ConfigurableInventory { - slots: Vec>, - /// The slots that the player can place items into for crafting. The - /// crafting result slot is always zero, and should not be included in this - /// range. - #[allow(dead_code)] // TODO: implement crafting - crafting_slots: Option>, - /// The type of window that should be used to display this inventory. - pub window_type: VarInt, - dirty: bool, -} - -impl ConfigurableInventory { - pub fn new(size: usize, window_type: VarInt, crafting_slots: Option>) -> Self { - ConfigurableInventory { - slots: vec![None; size], - crafting_slots, - window_type, - dirty: false, - } - } -} - -impl Inventory for ConfigurableInventory { - fn slot(&self, slot_id: SlotId) -> Option<&ItemStack> { - if !self.slot_range().contains(&slot_id) { - return None; - } - self.slots[slot_id as usize].as_ref() - } - - fn set_slot(&mut self, slot_id: SlotId, slot: Option) -> Option { - if !self.slot_range().contains(&slot_id) { - return None; - } - self.mark_dirty(true); - std::mem::replace(&mut self.slots[slot_id as usize], slot) - } - - fn slot_range(&self) -> Range { - 0..(self.slots.len() as SlotId) - } -} - -impl InventoryDirtyable for ConfigurableInventory { - fn mark_dirty(&mut self, dirty: bool) { - self.dirty = dirty - } - - fn is_dirty(&self) -> bool { - self.dirty - } -} -/// Represents what the player sees when they open an object's Inventory. -/// -/// This exists because when an object inventory screen is being shown to the -/// player, it also shows part of the player's inventory so they can move items -/// between the inventories. -pub struct WindowInventory { - pub window_id: u8, - pub object_inventory: InventoryId, -} - -impl WindowInventory { - pub fn new(window_id: impl Into, object_inventory: InventoryId) -> Self { - WindowInventory { - window_id: window_id.into(), - object_inventory, - } - } - - pub fn slots<'a>( - &self, - obj_inventory: &'a ConfigurableInventory, - player_inventory: &'a PlayerInventory, - ) -> Vec> { - let total_slots = obj_inventory.slots.len() + PlayerInventory::GENERAL_SLOTS.len(); - (0..total_slots) - .map(|s| { - if s < obj_inventory.slot_count() { - return obj_inventory.slot(s as SlotId); - } - let offset = obj_inventory.slot_count(); - player_inventory.slot((s - offset) as SlotId + PlayerInventory::GENERAL_SLOTS.start) - }) - .collect() - } -} - -#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Debug)] -pub struct InventoryId(Key); - -/// Manages all inventories that are present in the server. -pub struct Inventories { - slab: VersionedSlab, -} - -impl Inventories { +impl Inventories { pub(crate) fn new() -> Self { Self { slab: VersionedSlab::new(), } } - /// Creates a new inventory on a server. pub fn insert( &mut self, - inv: ConfigurableInventory, - ) -> (InventoryId, &mut ConfigurableInventory) { - let (key, value) = self.slab.insert(inv); - (InventoryId(key), value) + kind: InventoryKind, + title: impl Into, + state: C::InventoryState, + ) -> (InventoryId, &mut Inventory) { + let (id, inv) = self.slab.insert(Inventory { + state, + title: title.into(), + kind, + slots: vec![None; kind.slot_count()].into(), + modified: 0, + }); + + (InventoryId(id), inv) } - /// Removes an inventory from the server. - pub fn remove(&mut self, inv: InventoryId) -> Option { - self.slab.remove(inv.0) + pub fn remove(&mut self, id: InventoryId) -> Option { + self.slab.remove(id.0).map(|inv| inv.state) } - /// Returns the number of inventories in this container. - pub fn len(&self) -> usize { - self.slab.len() + pub fn get(&self, id: InventoryId) -> Option<&Inventory> { + self.slab.get(id.0) } - /// Returns `true` if there are no inventories. - pub fn is_empty(&self) -> bool { - self.slab.len() == 0 + pub fn get_mut(&mut self, id: InventoryId) -> Option<&mut Inventory> { + self.slab.get_mut(id.0) } - pub fn get(&self, inv: InventoryId) -> Option<&ConfigurableInventory> { - self.slab.get(inv.0) + pub fn iter( + &self, + ) -> impl ExactSizeIterator)> + FusedIterator + Clone + '_ + { + self.slab.iter().map(|(k, inv)| (InventoryId(k), inv)) } - pub fn get_mut(&mut self, inv: InventoryId) -> Option<&mut ConfigurableInventory> { - self.slab.get_mut(inv.0) + pub fn iter_mut( + &mut self, + ) -> impl ExactSizeIterator)> + FusedIterator + '_ { + self.slab.iter_mut().map(|(k, inv)| (InventoryId(k), inv)) } pub(crate) fn update(&mut self) { - // now that we have synced all the dirty inventories, mark them as clean - for (_, inv) in self.slab.iter_mut() { - inv.mark_dirty(false); + for (_, inv) in self.iter_mut() { + inv.modified = 0; } } } -#[derive(Copy, Clone, Debug, Error)] -#[error("InventoryError")] -pub struct InventoryError; +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Debug)] +pub struct InventoryId(Key); -#[cfg(test)] -mod test { - use valence_protocol::{ItemKind, ItemStack}; +impl InventoryId { + pub const NULL: Self = Self(Key::NULL); +} - use super::*; +pub struct Inventory { + /// Custom state + pub state: C::InventoryState, + title: Text, + kind: InventoryKind, + slots: Box<[Option]>, + /// Contains a set bit for each modified slot in `slots`. + modified: u64, +} - #[test] - fn test_get_set_slots() { - let mut inv = PlayerInventory::new(); - let slot = Some(ItemStack::new(ItemKind::Bone, 12, None)); - let prev = inv.set_slot(9, slot.clone()); - assert_eq!(inv.slot(9), slot.as_ref()); - assert_eq!(prev, None); - } +impl Deref for Inventory { + type Target = C::InventoryState; - #[test] - fn test_consume() { - let mut inv = PlayerInventory::new(); - let slot_id = 9; - let slot = Some(ItemStack::new(ItemKind::Bone, 12, None)); - inv.set_slot(slot_id, slot); - assert!(matches!(inv.consume(slot_id, 2), Ok(_))); - assert_eq!(inv.slot(slot_id).unwrap().count(), 10); - assert!(matches!(inv.consume(slot_id, 20), Err(_))); - assert_eq!(inv.slot(slot_id).unwrap().count(), 10); - assert!(matches!(inv.consume(slot_id, 10), Ok(_))); - assert_eq!(inv.slot(slot_id), None); + fn deref(&self) -> &Self::Target { + &self.state + } +} + +impl DerefMut for Inventory { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.state + } +} + +impl Inventory { + pub fn slot(&self, idx: u16) -> Option<&ItemStack> { + self.slots + .get(idx as usize) + .expect("slot index out of range") + .as_ref() + } + + pub fn replace_slot( + &mut self, + idx: u16, + item: impl Into>, + ) -> Option { + assert!(idx < self.slot_count(), "slot index out of range"); + + let new = item.into(); + let old = &mut self.slots[idx as usize]; + + if new != *old { + self.modified |= 1 << idx; + } + + mem::replace(old, new) + } + + pub fn slot_count(&self) -> u16 { + self.slots.len() as u16 + } + + pub fn slots( + &self, + ) -> impl ExactSizeIterator> + + DoubleEndedIterator + + FusedIterator + + Clone + + '_ { + self.slots.iter().map(|item| item.as_ref()) + } + + pub fn kind(&self) -> InventoryKind { + self.kind + } + + pub fn title(&self) -> &Text { + &self.title + } + + pub fn replace_title(&mut self, title: impl Into) -> Text { + // TODO: set title modified flag + mem::replace(&mut self.title, title.into()) + } + + pub(crate) fn slot_slice(&self) -> &[Option] { + self.slots.as_ref() + } + + pub(crate) fn send_update( + &self, + send: &mut PlayPacketSender, + window_id: u8, + state_id: &mut Wrapping, + ) -> anyhow::Result<()> { + if self.modified != 0 { + for (idx, slot) in self.slots.iter().enumerate() { + if (self.modified >> idx) & 1 == 1 { + *state_id += 1; + + send.append_packet(&SetContainerSlotEncode { + window_id: window_id as i8, + state_id: VarInt(state_id.0), + slot_idx: idx as i16, + slot_data: slot.as_ref(), + })?; + } + } + } + + Ok(()) } } diff --git a/src/lib.rs b/src/lib.rs index 2718616..8e00ce7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -111,7 +111,6 @@ pub mod entity; pub mod inventory; pub mod player_list; pub mod player_textures; -#[doc(hidden)] pub mod server; mod slab; mod slab_rc; @@ -125,13 +124,11 @@ pub mod world; pub mod prelude { pub use biome::{Biome, BiomeId}; pub use chunk::{Chunk, ChunkPos, Chunks, LoadedChunk, UnloadedChunk}; - pub use client::{handle_event_default, Client, ClientEvent, ClientId, Clients}; + pub use client::{Client, ClientEvent, ClientId, Clients}; pub use config::{Config, ConnectionMode, PlayerSampleEntry, ServerListPing}; pub use dimension::{Dimension, DimensionId}; pub use entity::{Entities, Entity, EntityEvent, EntityId, EntityKind, TrackedData}; - pub use inventory::{ - ConfigurableInventory, Inventories, Inventory, InventoryId, PlayerInventory, SlotId, - }; + pub use inventory::{Inventories, Inventory, InventoryId}; pub use player_list::{PlayerList, PlayerListEntry, PlayerListId, PlayerLists}; pub use server::{NewClientData, Server, SharedServer, ShutdownResult}; pub use spatial_index::{RaycastHit, SpatialIndex}; @@ -147,8 +144,8 @@ pub mod prelude { pub use valence_protocol::text::Color; pub use valence_protocol::types::{GameMode, Hand, SoundCategory}; pub use valence_protocol::{ - ident, translation_key, BlockKind, BlockPos, BlockState, Ident, ItemKind, ItemStack, Text, - TextFormat, Username, MINECRAFT_VERSION, PROTOCOL_VERSION, + ident, translation_key, BlockKind, BlockPos, BlockState, Ident, InventoryKind, ItemKind, + ItemStack, Text, TextFormat, Username, MINECRAFT_VERSION, PROTOCOL_VERSION, }; pub use vek::{Aabb, Mat2, Mat3, Mat4, Vec2, Vec3, Vec4}; pub use world::{World, WorldId, WorldMeta, Worlds}; diff --git a/src/player_list.rs b/src/player_list.rs index 1989aad..1e14919 100644 --- a/src/player_list.rs +++ b/src/player_list.rs @@ -2,6 +2,7 @@ use std::collections::hash_map::Entry; use std::collections::{HashMap, HashSet}; +use std::ops::{Deref, DerefMut}; use bitfield_struct::bitfield; use uuid::Uuid; @@ -11,12 +12,12 @@ use valence_protocol::{Text, VarInt}; use crate::config::Config; use crate::player_textures::SignedPlayerTextures; -use crate::server::PlayPacketController; -use crate::slab_rc::{Key, SlabRc}; +use crate::server::PlayPacketSender; +use crate::slab_rc::{Key, RcSlab}; /// A container for all [`PlayerList`]s on a server. pub struct PlayerLists { - slab: SlabRc>, + slab: RcSlab>, } /// An identifier for a [`PlayerList`] on the server. @@ -33,7 +34,7 @@ pub struct PlayerListId(Key); impl PlayerLists { pub(crate) fn new() -> Self { Self { - slab: SlabRc::new(), + slab: RcSlab::new(), } } @@ -110,6 +111,20 @@ pub struct PlayerList { modified_header_or_footer: bool, } +impl Deref for PlayerList { + type Target = C::PlayerListState; + + fn deref(&self) -> &Self::Target { + &self.state + } +} + +impl DerefMut for PlayerList { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.state + } +} + impl PlayerList { /// Inserts a player into the player list. /// @@ -243,10 +258,7 @@ impl PlayerList { self.entries.iter_mut().map(|(k, v)| (*k, v)) } - pub(crate) fn send_initial_packets( - &self, - ctrl: &mut PlayPacketController, - ) -> anyhow::Result<()> { + pub(crate) fn send_initial_packets(&self, send: &mut PlayPacketSender) -> anyhow::Result<()> { let add_player: Vec<_> = self .entries .iter() @@ -272,11 +284,11 @@ impl PlayerList { .collect(); if !add_player.is_empty() { - ctrl.append_packet(&PlayerInfo::AddPlayer(add_player))?; + send.append_packet(&PlayerInfo::AddPlayer(add_player))?; } if self.header != Text::default() || self.footer != Text::default() { - ctrl.append_packet(&SetTabListHeaderAndFooter { + send.append_packet(&SetTabListHeaderAndFooter { header: self.header.clone(), footer: self.footer.clone(), })?; @@ -285,12 +297,9 @@ impl PlayerList { Ok(()) } - pub(crate) fn send_update_packets( - &self, - ctrl: &mut PlayPacketController, - ) -> anyhow::Result<()> { + pub(crate) fn send_update_packets(&self, send: &mut PlayPacketSender) -> anyhow::Result<()> { if !self.removed.is_empty() { - ctrl.append_packet(&PlayerInfo::RemovePlayer( + send.append_packet(&PlayerInfo::RemovePlayer( self.removed.iter().cloned().collect(), ))?; } @@ -338,23 +347,23 @@ impl PlayerList { } if !add_player.is_empty() { - ctrl.append_packet(&PlayerInfo::AddPlayer(add_player))?; + send.append_packet(&PlayerInfo::AddPlayer(add_player))?; } if !game_mode.is_empty() { - ctrl.append_packet(&PlayerInfo::UpdateGameMode(game_mode))?; + send.append_packet(&PlayerInfo::UpdateGameMode(game_mode))?; } if !ping.is_empty() { - ctrl.append_packet(&PlayerInfo::UpdateLatency(ping))?; + send.append_packet(&PlayerInfo::UpdateLatency(ping))?; } if !display_name.is_empty() { - ctrl.append_packet(&PlayerInfo::UpdateDisplayName(display_name))?; + send.append_packet(&PlayerInfo::UpdateDisplayName(display_name))?; } if self.modified_header_or_footer { - ctrl.append_packet(&SetTabListHeaderAndFooter { + send.append_packet(&SetTabListHeaderAndFooter { header: self.header.clone(), footer: self.footer.clone(), })?; @@ -363,10 +372,7 @@ impl PlayerList { Ok(()) } - pub(crate) fn queue_clear_packets( - &self, - ctrl: &mut PlayPacketController, - ) -> anyhow::Result<()> { + pub(crate) fn queue_clear_packets(&self, ctrl: &mut PlayPacketSender) -> anyhow::Result<()> { ctrl.append_packet(&PlayerInfo::RemovePlayer( self.entries.keys().cloned().collect(), )) diff --git a/src/server.rs b/src/server.rs index c72e72e..6eadb8a 100644 --- a/src/server.rs +++ b/src/server.rs @@ -10,16 +10,16 @@ use std::{io, thread}; use anyhow::{ensure, Context}; use flume::{Receiver, Sender}; -pub(crate) use packet_controller::PlayPacketController; +pub(crate) use packet_manager::{PlayPacketReceiver, PlayPacketSender}; use rand::rngs::OsRng; use rayon::iter::ParallelIterator; -use reqwest::Client as HttpClient; +use reqwest::Client as ReqwestClient; use rsa::{PublicKeyParts, RsaPrivateKey}; use serde_json::{json, Value}; use tokio::net::tcp::{OwnedReadHalf, OwnedWriteHalf}; use tokio::net::{TcpListener, TcpStream}; use tokio::runtime::{Handle, Runtime}; -use tokio::sync::Semaphore; +use tokio::sync::{OwnedSemaphorePermit, Semaphore}; use tracing::{error, info, info_span, instrument, trace, warn}; use uuid::Uuid; use valence_nbt::{compound, Compound, List}; @@ -41,16 +41,20 @@ use crate::entity::Entities; use crate::inventory::Inventories; use crate::player_list::PlayerLists; use crate::player_textures::SignedPlayerTextures; -use crate::server::packet_controller::InitialPacketController; +use crate::server::packet_manager::InitialPacketManager; use crate::world::Worlds; use crate::Ticks; mod byte_channel; mod login; -mod packet_controller; +mod packet_manager; /// Contains the entire state of a running Minecraft server, accessible from -/// within the [update](crate::config::Config::update) loop. +/// within the [init] and [update] functions. +/// +/// [init]: crate::config::Config::init +/// [update]: crate::config::Config::update +#[non_exhaustive] pub struct Server { /// Custom state. pub state: C::ServerState, @@ -65,7 +69,7 @@ pub struct Server { /// All of the player lists on the server. pub player_lists: PlayerLists, /// All of the inventories on the server. - pub inventories: Inventories, + pub inventories: Inventories, } /// A handle to a Minecraft server containing the subset of functionality which @@ -119,26 +123,28 @@ struct SharedServerInner { /// This is sent to clients during the authentication process. public_key_der: Box<[u8]>, /// For session server requests. - http_client: HttpClient, + http_client: ReqwestClient, } -/// Contains information about a new client. +/// Contains information about a new client joining the server. #[non_exhaustive] pub struct NewClientData { - /// The UUID of the new client. - pub uuid: Uuid, /// The username of the new client. pub username: Username, + /// The UUID of the new client. + pub uuid: Uuid, + /// The remote address of the new client. + pub ip: IpAddr, /// The new client's player textures. May be `None` if the client does not /// have a skin or cape. pub textures: Option, - /// The remote address of the new client. - pub remote_addr: IpAddr, } struct NewClientMessage { ncd: NewClientData, - ctrl: PlayPacketController, + send: PlayPacketSender, + recv: PlayPacketReceiver, + permit: OwnedSemaphorePermit, } /// The result type returned from [`start_server`]. @@ -359,7 +365,7 @@ fn setup_server(cfg: C) -> anyhow::Result> { shutdown_result: Mutex::new(None), rsa_key, public_key_der, - http_client: HttpClient::new(), + http_client: ReqwestClient::new(), }; Ok(SharedServer(Arc::new(server))) @@ -408,20 +414,23 @@ fn do_update_loop(server: &mut Server) -> ShutdownResult { info!( username = %msg.ncd.username, uuid = %msg.ncd.uuid, - ip = %msg.ncd.remote_addr, + ip = %msg.ncd.ip, "inserting client" ); - server - .clients - .insert(Client::new(msg.ctrl, msg.ncd, Default::default())); + server.clients.insert(Client::new( + msg.send, + msg.recv, + msg.permit, + msg.ncd, + Default::default(), + )); } // Get serverbound packets first so they are not dealt with a tick late. - - server.clients.par_iter_mut().for_each(|(_, client)| { - client.handle_serverbound_packets(&server.entities); - }); + for (_, client) in server.clients.iter_mut() { + client.prepare_c2s_packets(); + } info_span!("configured_update").in_scope(|| shared.config().update(server)); @@ -472,11 +481,12 @@ async fn do_accept_loop(server: SharedServer) { match server.0.connection_sema.clone().acquire_owned().await { Ok(permit) => match listener.accept().await { Ok((stream, remote_addr)) => { - let server = server.clone(); - tokio::spawn(async move { - handle_connection(server, stream, remote_addr).await; - drop(permit); - }); + tokio::spawn(handle_connection( + server.clone(), + stream, + remote_addr, + permit, + )); } Err(e) => { error!("failed to accept incoming connection: {e}"); @@ -493,6 +503,7 @@ async fn handle_connection( server: SharedServer, stream: TcpStream, remote_addr: SocketAddr, + permit: OwnedSemaphorePermit, ) { trace!("handling connection"); @@ -502,17 +513,18 @@ async fn handle_connection( let (read, write) = stream.into_split(); - let ctrl = InitialPacketController::new( + let mngr = InitialPacketManager::new( read, write, PacketEncoder::new(), PacketDecoder::new(), Duration::from_secs(5), + permit, ); // TODO: peek stream for 0xFE legacy ping - if let Err(e) = handle_handshake(server, ctrl, remote_addr).await { + if let Err(e) = handle_handshake(server, mngr, remote_addr).await { // EOF can happen if the client disconnects while joining, which isn't // very erroneous. if let Some(e) = e.downcast_ref::() { @@ -526,10 +538,10 @@ async fn handle_connection( async fn handle_handshake( server: SharedServer, - mut ctrl: InitialPacketController, + mut mngr: InitialPacketManager, remote_addr: SocketAddr, ) -> anyhow::Result<()> { - let handshake = ctrl.recv_packet::().await?; + let handshake = mngr.recv_packet::().await?; ensure!( matches!(server.connection_mode(), ConnectionMode::BungeeCord) @@ -538,21 +550,25 @@ async fn handle_handshake( ); match handshake.next_state { - HandshakeNextState::Status => handle_status(server, ctrl, remote_addr, handshake) + HandshakeNextState::Status => handle_status(server, mngr, remote_addr, handshake) .await .context("error handling status"), - HandshakeNextState::Login => match handle_login(&server, &mut ctrl, remote_addr, handshake) + HandshakeNextState::Login => match handle_login(&server, &mut mngr, remote_addr, handshake) .await .context("error handling login")? { Some(ncd) => { + let (send, recv, permit) = mngr.into_play( + server.0.incoming_capacity, + server.0.outgoing_capacity, + server.tokio_handle().clone(), + ); + let msg = NewClientMessage { ncd, - ctrl: ctrl.into_play_packet_controller( - server.0.incoming_capacity, - server.0.outgoing_capacity, - server.tokio_handle().clone(), - ), + send, + recv, + permit, }; let _ = server.0.new_clients_tx.send_async(msg).await; @@ -565,11 +581,11 @@ async fn handle_handshake( async fn handle_status( server: SharedServer, - mut ctrl: InitialPacketController, + mut mngr: InitialPacketManager, remote_addr: SocketAddr, handshake: HandshakeOwned, ) -> anyhow::Result<()> { - ctrl.recv_packet::().await?; + mngr.recv_packet::().await?; match server .0 @@ -605,7 +621,7 @@ async fn handle_status( .insert("favicon".to_owned(), Value::String(buf)); } - ctrl.send_packet(&StatusResponse { + mngr.send_packet(&StatusResponse { json: &json.to_string(), }) .await?; @@ -613,9 +629,9 @@ async fn handle_status( ServerListPing::Ignore => return Ok(()), } - let PingRequest { payload } = ctrl.recv_packet().await?; + let PingRequest { payload } = mngr.recv_packet().await?; - ctrl.send_packet(&PingResponse { payload }).await?; + mngr.send_packet(&PingResponse { payload }).await?; Ok(()) } @@ -623,7 +639,7 @@ async fn handle_status( /// Handle the login process and return the new client's data if successful. async fn handle_login( server: &SharedServer, - ctrl: &mut InitialPacketController, + mngr: &mut InitialPacketManager, remote_addr: SocketAddr, handshake: HandshakeOwned, ) -> anyhow::Result> { @@ -636,33 +652,33 @@ async fn handle_login( username, sig_data: _, // TODO profile_id: _, // TODO - } = ctrl.recv_packet().await?; + } = mngr.recv_packet().await?; let username = username.to_owned_username(); let ncd = match server.connection_mode() { - ConnectionMode::Online => login::online(server, ctrl, remote_addr, username).await?, + ConnectionMode::Online => login::online(server, mngr, remote_addr, username).await?, ConnectionMode::Offline => login::offline(remote_addr, username)?, ConnectionMode::BungeeCord => login::bungeecord(&handshake.server_address, username)?, - ConnectionMode::Velocity { secret } => login::velocity(ctrl, username, secret).await?, + ConnectionMode::Velocity { secret } => login::velocity(mngr, username, secret).await?, }; if let Some(threshold) = server.0.cfg.compression_threshold() { - ctrl.send_packet(&SetCompression { + mngr.send_packet(&SetCompression { threshold: VarInt(threshold as i32), }) .await?; - ctrl.set_compression(Some(threshold)); + mngr.set_compression(Some(threshold)); } if let Err(reason) = server.0.cfg.login(server, &ncd).await { info!("disconnect at login: \"{reason}\""); - ctrl.send_packet(&DisconnectLogin { reason }).await?; + mngr.send_packet(&DisconnectLogin { reason }).await?; return Ok(None); } - ctrl.send_packet(&LoginSuccess { + mngr.send_packet(&LoginSuccess { uuid: ncd.uuid, username: ncd.username.as_str_username(), properties: Vec::new(), diff --git a/src/server/login.rs b/src/server/login.rs index 4c4068c..d976f93 100644 --- a/src/server/login.rs +++ b/src/server/login.rs @@ -24,14 +24,14 @@ use valence_protocol::{translation_key, Decode, Ident, RawBytes, Text, Username, use crate::config::Config; use crate::player_textures::SignedPlayerTextures; -use crate::server::packet_controller::InitialPacketController; +use crate::server::packet_manager::InitialPacketManager; use crate::server::{NewClientData, SharedServer}; /// Login sequence for /// [`ConnectionMode::Online`](crate::config::ConnectionMode). pub(super) async fn online( server: &SharedServer, - ctrl: &mut InitialPacketController, + ctrl: &mut InitialPacketManager, remote_addr: SocketAddr, username: Username, ) -> anyhow::Result { @@ -132,7 +132,7 @@ pub(super) async fn online( uuid, username, textures: Some(textures), - remote_addr: remote_addr.ip(), + ip: remote_addr.ip(), }) } @@ -147,7 +147,7 @@ pub(super) fn offline( uuid: Uuid::from_slice(&Sha256::digest(username.as_str())[..16])?, username, textures: None, - remote_addr: remote_addr.ip(), + ip: remote_addr.ip(), }) } @@ -188,7 +188,7 @@ pub(super) fn bungeecord( uuid: uuid.parse()?, username, textures, - remote_addr: client_ip.parse()?, + ip: client_ip.parse()?, }) } @@ -197,7 +197,7 @@ fn auth_digest(bytes: &[u8]) -> String { } pub(super) async fn velocity( - ctrl: &mut InitialPacketController, + ctrl: &mut InitialPacketManager, username: Username, velocity_secret: &str, ) -> anyhow::Result { @@ -279,7 +279,7 @@ pub(super) async fn velocity( uuid, username, textures, - remote_addr, + ip: remote_addr, }) } diff --git a/src/server/packet_controller.rs b/src/server/packet_manager.rs similarity index 85% rename from src/server/packet_controller.rs rename to src/server/packet_manager.rs index abd4e02..2909c9c 100644 --- a/src/server/packet_controller.rs +++ b/src/server/packet_manager.rs @@ -5,6 +5,7 @@ use anyhow::Result; use tokio::io; use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; use tokio::runtime::Handle; +use tokio::sync::OwnedSemaphorePermit; use tokio::task::JoinHandle; use tokio::time::timeout; use tracing::debug; @@ -12,17 +13,18 @@ use valence_protocol::{Decode, Encode, Packet, PacketDecoder, PacketEncoder}; use crate::server::byte_channel::{byte_channel, ByteReceiver, ByteSender, TryRecvError}; -pub struct InitialPacketController { +pub struct InitialPacketManager { reader: R, writer: W, enc: PacketEncoder, dec: PacketDecoder, timeout: Duration, + permit: OwnedSemaphorePermit, } const READ_BUF_SIZE: usize = 4096; -impl InitialPacketController +impl InitialPacketManager where R: AsyncRead + Unpin, W: AsyncWrite + Unpin, @@ -33,6 +35,7 @@ where enc: PacketEncoder, dec: PacketDecoder, timeout: Duration, + permit: OwnedSemaphorePermit, ) -> Self { Self { reader, @@ -40,6 +43,7 @@ where enc, dec, timeout, + permit, } } @@ -74,6 +78,7 @@ where Ok(self .dec .try_next_packet()? + // TODO: this panicked after a timeout. .expect("decoder said it had another packet")) // The following is what I want to write but can't due to borrow @@ -111,12 +116,12 @@ where self.dec.enable_encryption(key); } - pub fn into_play_packet_controller( + pub fn into_play( mut self, incoming_limit: usize, outgoing_limit: usize, handle: Handle, - ) -> PlayPacketController + ) -> (PlayPacketSender, PlayPacketReceiver, OwnedSemaphorePermit) where R: Send + 'static, W: Send + 'static, @@ -162,32 +167,33 @@ where } }); - PlayPacketController { - enc: self.enc, - dec: self.dec, - send: outgoing_sender, - recv: incoming_receiver, - reader_task, - writer_task: Some(writer_task), - handle, - } + ( + PlayPacketSender { + enc: self.enc, + send: outgoing_sender, + writer_task: Some(writer_task), + handle, + }, + PlayPacketReceiver { + dec: self.dec, + recv: incoming_receiver, + reader_task, + }, + self.permit, + ) } } -/// A convenience structure for managing a pair of packet encoder/decoders and -/// the byte channels from which to send and receive the packet data during the -/// play state. -pub struct PlayPacketController { +/// Manages a packet encoder and a byte channel to send the encoded packets +/// through. +pub struct PlayPacketSender { enc: PacketEncoder, - dec: PacketDecoder, send: ByteSender, - recv: ByteReceiver, - reader_task: JoinHandle<()>, writer_task: Option>, handle: Handle, } -impl PlayPacketController { +impl PlayPacketSender { pub fn append_packet

(&mut self, pkt: &P) -> Result<()> where P: Encode + Packet + ?Sized, @@ -202,6 +208,42 @@ impl PlayPacketController { self.enc.prepend_packet(pkt) } + #[allow(dead_code)] + pub fn set_compression(&mut self, threshold: Option) { + self.enc.set_compression(threshold) + } + + pub fn flush(&mut self) -> Result<()> { + let bytes = self.enc.take(); + self.send.try_send(bytes)?; + Ok(()) + } +} + +impl Drop for PlayPacketSender { + fn drop(&mut self) { + let _ = self.flush(); + + if let Some(writer_task) = self.writer_task.take() { + if !writer_task.is_finished() { + let _guard = self.handle.enter(); + + // Give any unsent packets a moment to send before we cut the connection. + self.handle + .spawn(timeout(Duration::from_secs(1), writer_task)); + } + } + } +} + +/// Manages a packet decoder and a byte channel to receive the encoded packets. +pub struct PlayPacketReceiver { + dec: PacketDecoder, + recv: ByteReceiver, + reader_task: JoinHandle<()>, +} + +impl PlayPacketReceiver { pub fn try_next_packet<'a, P>(&'a mut self) -> Result> where P: Decode<'a> + Packet, @@ -220,33 +262,10 @@ impl PlayPacketController { Err(TryRecvError::Disconnected) => false, } } - - #[allow(dead_code)] - pub fn set_compression(&mut self, threshold: Option) { - self.enc.set_compression(threshold) - } - - pub fn flush(&mut self) -> Result<()> { - let bytes = self.enc.take(); - self.send.try_send(bytes)?; - Ok(()) - } } -impl Drop for PlayPacketController { +impl Drop for PlayPacketReceiver { fn drop(&mut self) { self.reader_task.abort(); - - let _ = self.flush(); - - if let Some(writer_task) = self.writer_task.take() { - if !writer_task.is_finished() { - let _guard = self.handle.enter(); - - // Give any unsent packets a moment to send before we cut the connection. - self.handle - .spawn(timeout(Duration::from_secs(1), writer_task)); - } - } } } diff --git a/src/slab_rc.rs b/src/slab_rc.rs index 40da6ce..935725c 100644 --- a/src/slab_rc.rs +++ b/src/slab_rc.rs @@ -10,7 +10,7 @@ use rayon::iter::{IntoParallelRefIterator, IntoParallelRefMutIterator, ParallelI use crate::slab::Slab; #[derive(Clone, Debug)] -pub struct SlabRc { +pub struct RcSlab { slab: Slab>, } @@ -56,7 +56,7 @@ impl Hash for Key { } } -impl SlabRc { +impl RcSlab { pub const fn new() -> Self { Self { slab: Slab::new() } } diff --git a/src/world.rs b/src/world.rs index 0a92ee8..8f5eac5 100644 --- a/src/world.rs +++ b/src/world.rs @@ -1,6 +1,7 @@ //! A space on a server for objects to occupy. use std::iter::FusedIterator; +use std::ops::{Deref, DerefMut}; use rayon::iter::ParallelIterator; @@ -66,11 +67,8 @@ impl Worlds { /// Note that any entities located in the world are not deleted. /// Additionally, clients that are still in the deleted world at the end /// of the tick are disconnected. - /// - /// Returns `true` if the world was deleted. Otherwise, `false` is returned - /// and the function has no effect. - pub fn remove(&mut self, world: WorldId) -> bool { - self.slab.remove(world.0).is_some() + pub fn remove(&mut self, world: WorldId) -> Option { + self.slab.remove(world.0).map(|w| w.state) } /// Removes all worlds from the server for which `f` returns `false`. @@ -143,6 +141,20 @@ pub struct World { pub meta: WorldMeta, } +impl Deref for World { + type Target = C::WorldState; + + fn deref(&self) -> &Self::Target { + &self.state + } +} + +impl DerefMut for World { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.state + } +} + /// Contains miscellaneous data about the world. pub struct WorldMeta { dimension: DimensionId, diff --git a/valence_protocol/src/ident.rs b/valence_protocol/src/ident.rs index 5a021cd..4cb2bfe 100644 --- a/valence_protocol/src/ident.rs +++ b/valence_protocol/src/ident.rs @@ -1,5 +1,6 @@ //! Resource identifiers. +use std::borrow::Cow; use std::cmp::Ordering; use std::error::Error; use std::fmt; @@ -101,11 +102,6 @@ impl> Ident { pub fn into_inner(self) -> S { self.string } - - /// Consumes the identifier and returns the underlying string. - pub fn get(self) -> S { - self.string - } } impl<'a, S: ?Sized> Ident<&'a S> { @@ -124,6 +120,39 @@ impl<'a, S: ?Sized> Ident<&'a S> { } } +impl<'a> From> for Ident { + fn from(value: Ident<&'a str>) -> Self { + value.to_owned_ident() + } +} + +impl<'a> From> for Ident> { + fn from(value: Ident<&'a str>) -> Self { + Ident { + string: value.string.into(), + path_start: value.path_start, + } + } +} + +impl<'a> From> for Ident> { + fn from(value: Ident<&'a str>) -> Self { + Ident { + string: Cow::Borrowed(value.string), + path_start: value.path_start, + } + } +} + +impl<'a> From>> for Ident { + fn from(value: Ident>) -> Self { + Ident { + string: value.string.into_owned(), + path_start: value.path_start, + } + } +} + impl FromStr for Ident { type Err = IdentError; diff --git a/valence_protocol/src/inventory.rs b/valence_protocol/src/inventory.rs new file mode 100644 index 0000000..14e8657 --- /dev/null +++ b/valence_protocol/src/inventory.rs @@ -0,0 +1,62 @@ +use valence_derive::{Decode, Encode}; + +#[derive(Copy, Clone, PartialEq, Eq, Debug, Encode, Decode)] +pub enum InventoryKind { + Generic9x1, + Generic9x2, + Generic9x3, + Generic9x4, + Generic9x5, + Generic9x6, + Generic3x3, + Anvil, + Beacon, + BlastFurnace, + BrewingStand, + Crafting, + Enchantment, + Furnace, + Grindstone, + Hopper, + Lectern, + Loom, + Merchant, + ShulkerBox, + Smithing, + Smoker, + Cartography, + Stonecutter, +} + +impl InventoryKind { + /// The number of slots in this inventory, not counting the player's main + /// inventory slots. + pub const fn slot_count(self) -> usize { + match self { + InventoryKind::Generic9x1 => 9, + InventoryKind::Generic9x2 => 9 * 2, + InventoryKind::Generic9x3 => 9 * 3, + InventoryKind::Generic9x4 => 9 * 4, + InventoryKind::Generic9x5 => 9 * 5, + InventoryKind::Generic9x6 => 9 * 6, + InventoryKind::Generic3x3 => 3 * 3, + InventoryKind::Anvil => 4, + InventoryKind::Beacon => 1, + InventoryKind::BlastFurnace => 3, + InventoryKind::BrewingStand => 5, + InventoryKind::Crafting => 10, + InventoryKind::Enchantment => 2, + InventoryKind::Furnace => 3, + InventoryKind::Grindstone => 3, + InventoryKind::Hopper => 5, + InventoryKind::Lectern => 1, + InventoryKind::Loom => 4, + InventoryKind::Merchant => 3, + InventoryKind::ShulkerBox => 27, + InventoryKind::Smithing => 3, + InventoryKind::Smoker => 3, + InventoryKind::Cartography => 3, + InventoryKind::Stonecutter => 2, + } + } +} diff --git a/valence_protocol/src/item.rs b/valence_protocol/src/item.rs index 630130b..75b247b 100644 --- a/valence_protocol/src/item.rs +++ b/valence_protocol/src/item.rs @@ -39,8 +39,18 @@ impl ItemStack { } impl Encode for Option { + fn encode(&self, w: impl Write) -> Result<()> { + self.as_ref().encode(w) + } + + fn encoded_len(&self) -> usize { + self.as_ref().encoded_len() + } +} + +impl<'a> Encode for Option<&'a ItemStack> { fn encode(&self, mut w: impl Write) -> Result<()> { - match self { + match *self { None => false.encode(w), Some(s) => { true.encode(&mut w)?; @@ -55,7 +65,7 @@ impl Encode for Option { } fn encoded_len(&self) -> usize { - match self { + match *self { None => 1, Some(s) => { 1 + s.item.encoded_len() diff --git a/valence_protocol/src/lib.rs b/valence_protocol/src/lib.rs index e6fa785..1bf8140 100644 --- a/valence_protocol/src/lib.rs +++ b/valence_protocol/src/lib.rs @@ -78,6 +78,7 @@ pub use byte_angle::ByteAngle; pub use cache::{Cached, EncodedBuf}; pub use codec::{PacketDecoder, PacketEncoder}; pub use ident::Ident; +pub use inventory::InventoryKind; pub use item::{ItemKind, ItemStack}; pub use raw_bytes::RawBytes; pub use text::{Text, TextFormat}; @@ -108,6 +109,7 @@ pub mod enchant; pub mod entity_meta; pub mod ident; mod impls; +mod inventory; mod item; pub mod packets; mod raw_bytes; diff --git a/valence_protocol/src/packets/c2s.rs b/valence_protocol/src/packets/c2s.rs index 7cdb65c..cd7a784 100644 --- a/valence_protocol/src/packets/c2s.rs +++ b/valence_protocol/src/packets/c2s.rs @@ -7,7 +7,7 @@ use crate::item::ItemStack; use crate::raw_bytes::RawBytes; use crate::types::{ Action, ChatMode, ClickContainerMode, CommandArgumentSignature, CommandBlockFlags, - CommandBlockMode, DiggingStatus, DisplayedSkinParts, EntityInteraction, Hand, + CommandBlockMode, Difficulty, DiggingStatus, DisplayedSkinParts, EntityInteraction, Hand, HandshakeNextState, MainHand, MessageAcknowledgment, MsgSigOrVerifyToken, PlayerInputFlags, PublicKeyData, RecipeBookId, StructureBlockAction, StructureBlockFlags, StructureBlockMirror, StructureBlockMode, StructureBlockRotation, @@ -116,17 +116,12 @@ pub mod play { #[packet_id = 0x01] pub struct QueryBlockEntityTag { pub transaction_id: VarInt, - pub location: BlockPos, + pub position: BlockPos, } #[derive(Copy, Clone, Debug, Encode, Decode, Packet)] #[packet_id = 0x02] - pub enum ChangeDifficulty { - Peaceful, - Easy, - Normal, - Hard, - } + pub struct ChangeDifficulty(pub Difficulty); #[derive(Clone, Debug, Encode, Decode, Packet)] #[packet_id = 0x03] @@ -164,7 +159,7 @@ pub mod play { #[packet_id = 0x07] pub enum ClientCommand { PerformRespawn, - RequestStatus, + RequestStats, } #[derive(Clone, Debug, Encode, Decode, Packet)] @@ -209,7 +204,7 @@ pub mod play { #[derive(Copy, Clone, Debug, Encode, Decode, Packet)] #[packet_id = 0x0c] pub struct CloseContainerC2s { - pub window_id: u8, + pub window_id: i8, } #[derive(Copy, Clone, Debug, Encode, Decode, Packet)] @@ -245,7 +240,7 @@ pub mod play { #[derive(Copy, Clone, Debug, Encode, Decode, Packet)] #[packet_id = 0x11] pub struct JigsawGenerate { - pub location: BlockPos, + pub position: BlockPos, pub levels: VarInt, pub keep_jigsaws: bool, } @@ -258,9 +253,7 @@ pub mod play { #[derive(Copy, Clone, Debug, Encode, Decode, Packet)] #[packet_id = 0x13] - pub struct LockDifficulty { - pub locked: bool, - } + pub struct LockDifficulty(pub bool); #[derive(Copy, Clone, Debug, Encode, Decode, Packet)] #[packet_id = 0x14] @@ -332,7 +325,7 @@ pub mod play { #[packet_id = 0x1d] pub struct PlayerAction { pub status: DiggingStatus, - pub location: BlockPos, + pub position: BlockPos, pub face: BlockFace, pub sequence: VarInt, } @@ -417,7 +410,7 @@ pub mod play { #[derive(Copy, Clone, Debug, Encode, Decode, Packet)] #[packet_id = 0x29] pub struct ProgramCommandBlock<'a> { - pub location: BlockPos, + pub position: BlockPos, pub command: &'a str, pub mode: CommandBlockMode, pub flags: CommandBlockFlags, @@ -441,7 +434,7 @@ pub mod play { #[derive(Copy, Clone, Debug, Encode, Decode, Packet)] #[packet_id = 0x2c] pub struct ProgramJigsawBlock<'a> { - pub location: BlockPos, + pub position: BlockPos, pub name: Ident<&'a str>, pub target: Ident<&'a str>, pub pool: Ident<&'a str>, @@ -452,7 +445,7 @@ pub mod play { #[derive(Copy, Clone, Debug, Encode, Decode, Packet)] #[packet_id = 0x2d] pub struct ProgramStructureBlock<'a> { - pub location: BlockPos, + pub position: BlockPos, pub action: StructureBlockAction, pub mode: StructureBlockMode, pub name: &'a str, @@ -469,7 +462,7 @@ pub mod play { #[derive(Copy, Clone, Debug, Encode, Decode, Packet)] #[packet_id = 0x2e] pub struct UpdateSign<'a> { - pub location: BlockPos, + pub position: BlockPos, pub lines: [&'a str; 4], } @@ -487,7 +480,7 @@ pub mod play { #[packet_id = 0x31] pub struct UseItemOn { pub hand: Hand, - pub location: BlockPos, + pub position: BlockPos, pub face: BlockFace, pub cursor_pos: [f32; 3], pub head_inside_block: bool, diff --git a/valence_protocol/src/packets/s2c.rs b/valence_protocol/src/packets/s2c.rs index 515ade6..342bcfc 100644 --- a/valence_protocol/src/packets/s2c.rs +++ b/valence_protocol/src/packets/s2c.rs @@ -145,14 +145,14 @@ pub mod play { #[packet_id = 0x06] pub struct SetBlockDestroyStage { pub entity_id: VarInt, - pub location: BlockPos, + pub position: BlockPos, pub destroy_stage: u8, } #[derive(Clone, Debug, Encode, Decode, Packet)] #[packet_id = 0x07] pub struct BlockEntityData { - pub location: BlockPos, + pub position: BlockPos, // TODO: BlockEntityKind enum? pub kind: VarInt, pub data: Compound, @@ -161,7 +161,7 @@ pub mod play { #[derive(Copy, Clone, Debug, Encode, Decode, Packet)] #[packet_id = 0x09] pub struct BlockUpdate { - pub location: BlockPos, + pub position: BlockPos, pub block_id: VarInt, } @@ -194,6 +194,15 @@ pub mod play { pub carried_item: Option, } + #[derive(Copy, Clone, Debug, Encode, Packet)] + #[packet_id = 0x11] + pub struct SetContainerContentEncode<'a> { + pub window_id: u8, + pub state_id: VarInt, + pub slots: &'a [Option], + pub carried_item: &'a Option, + } + #[derive(Copy, Clone, Debug, Encode, Decode, Packet)] #[packet_id = 0x12] pub struct SetContainerProperty { @@ -211,6 +220,15 @@ pub mod play { pub slot_data: Option, } + #[derive(Clone, Debug, Encode, Packet)] + #[packet_id = 0x13] + pub struct SetContainerSlotEncode<'a> { + pub window_id: i8, + pub state_id: VarInt, + pub slot_idx: i16, + pub slot_data: Option<&'a ItemStack>, + } + #[derive(Copy, Clone, Debug, Encode, Decode, Packet)] #[packet_id = 0x14] pub struct SetCooldown { @@ -512,7 +530,7 @@ pub mod play { #[derive(Copy, Clone, Debug, Encode, Decode, Packet)] #[packet_id = 0x4d] pub struct SetDefaultSpawnPosition { - pub location: BlockPos, + pub position: BlockPos, pub angle: f32, } diff --git a/valence_protocol/src/types.rs b/valence_protocol/src/types.rs index 6425068..6d4e2c2 100644 --- a/valence_protocol/src/types.rs +++ b/valence_protocol/src/types.rs @@ -91,7 +91,7 @@ pub enum DiggingStatus { FinishedDigging, DropItemStack, DropItem, - ShootArrowOrFinishEating, + UpdateHeldItemState, SwapItemInHand, }