inventory client events (#66)

* finish packet definition for ClickContainer

* add CloseScreen client event

* add DropItem client event

* add SetSlotCreative client event

* implement ClientEvent::CloseScreen

* ClientEvent::DropItem

* implement ClientEvent::SetSlotCreative

* cargo fmt

* add inventory_piano example to demo inventory slot click events

* lints

* implement ClickContainer event

* inventory_piano: deduplicate note playing logic

* add DropItemStack client event

* implement ClientEvent::DropItemStack

* adjust logging

* tweak inventory_piano example, send text to chat instead of stdout

* fix lint

* move Slot outside of protocol module

* avoid cloning slot in ClickContainer packet handler

* fix inventory_piano example
This commit is contained in:
Carson McManus 2022-09-19 14:29:41 -04:00 committed by GitHub
parent 2f2bc91535
commit 4574e18d49
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 371 additions and 7 deletions

249
examples/inventory_piano.rs Normal file
View file

@ -0,0 +1,249 @@
use std::net::SocketAddr;
use std::sync::atomic::{AtomicUsize, Ordering};
use log::LevelFilter;
use num::Integer;
use valence::block::BlockState;
use valence::chunk::{Chunk, UnloadedChunk};
use valence::client::{handle_event_default, Client, ClientEvent, GameMode};
use valence::config::{Config, ServerListPing};
use valence::dimension::{Dimension, DimensionId};
use valence::entity::{Entity, EntityId, EntityKind};
use valence::player_list::PlayerListId;
use valence::protocol::packets::c2s::play::ClickContainerMode;
use valence::protocol::packets::s2c::play::SoundCategory;
use valence::server::{Server, SharedServer, ShutdownResult};
use valence::slot::SlotId;
use valence::text::{Color, TextFormat};
use valence::{async_trait, ident};
pub fn main() -> ShutdownResult {
env_logger::Builder::new()
.filter_module("valence", LevelFilter::Trace)
.parse_default_env()
.init();
valence::start_server(
Game {
player_count: AtomicUsize::new(0),
},
ServerState { player_list: None },
)
}
struct Game {
player_count: AtomicUsize,
}
struct ServerState {
player_list: Option<PlayerListId>,
}
#[derive(Default)]
struct ClientState {
entity_id: EntityId,
}
const MAX_PLAYERS: usize = 10;
const SIZE_X: usize = 100;
const SIZE_Z: usize = 100;
const SLOT_MIN: SlotId = 36;
const SLOT_MAX: SlotId = 43;
const PITCH_MIN: f32 = 0.5;
const PITCH_MAX: f32 = 1.0;
#[async_trait]
impl Config for Game {
type ServerState = ServerState;
type ClientState = ClientState;
type EntityState = ();
type WorldState = ();
type ChunkState = ();
type PlayerListState = ();
fn max_connections(&self) -> usize {
// We want status pings to be successful even if the server is full.
MAX_PLAYERS + 64
}
fn dimensions(&self) -> Vec<Dimension> {
vec![Dimension {
fixed_time: Some(6000),
..Dimension::default()
}]
}
async fn server_list_ping(
&self,
_server: &SharedServer<Self>,
_remote_addr: SocketAddr,
_protocol_version: i32,
) -> ServerListPing {
ServerListPing::Respond {
online_players: self.player_count.load(Ordering::SeqCst) as i32,
max_players: MAX_PLAYERS as i32,
player_sample: Default::default(),
description: "Hello Valence!".color(Color::AQUA),
favicon_png: Some(include_bytes!("../assets/logo-64x64.png").as_slice().into()),
}
}
fn init(&self, server: &mut Server<Self>) {
let world = server.worlds.insert(DimensionId::default(), ()).1;
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
.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);
}
}
}
}
}
}
fn update(&self, server: &mut Server<Self>) {
let (world_id, _) = server.worlds.iter_mut().next().unwrap();
let spawn_pos = [SIZE_X as f64 / 2.0, 1.0, SIZE_Z as f64 / 2.0];
server.clients.retain(|_, client| {
if client.created_this_tick() {
if self
.player_count
.fetch_update(Ordering::SeqCst, Ordering::SeqCst, |count| {
(count < MAX_PLAYERS).then_some(count + 1)
})
.is_err()
{
client.disconnect("The server is full!".color(Color::RED));
return false;
}
match server
.entities
.insert_with_uuid(EntityKind::Player, client.uuid(), ())
{
Some((id, _)) => client.state.entity_id = id,
None => {
client.disconnect("Conflicting UUID");
return false;
}
}
client.spawn(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 {
server.player_lists.get_mut(id).insert(
client.uuid(),
client.username(),
client.textures().cloned(),
client.game_mode(),
0,
None,
);
}
client.set_game_mode(GameMode::Creative);
client.send_message(
"Welcome to Valence! Open your inventory, and click on your hotbar to play \
the piano."
.italic(),
);
client.send_message(
"Click the rightmost hotbar slot to toggle between creative and survival."
.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) {
match event {
ClientEvent::CloseScreen { .. } => {
client.send_message("Done already?");
}
ClientEvent::SetSlotCreative { slot_id, .. } => {
client.send_message(format!("{:#?}", event));
// If the user does a double click, 3 notes will be played.
// This is not possible to fix :(
play_note(client, player, slot_id);
}
ClientEvent::ClickContainer { slot_id, mode, .. } => {
client.send_message(format!("{:#?}", event));
if mode != ClickContainerMode::Click {
// Prevent notes from being played twice if the user clicks quickly
continue;
}
play_note(client, player, slot_id);
}
_ => {}
}
}
true
});
}
}
fn play_note(client: &mut Client<Game>, player: &mut Entity<Game>, clicked_slot: SlotId) {
if (SLOT_MIN..=SLOT_MAX).contains(&clicked_slot) {
let pitch = (clicked_slot - SLOT_MIN) as f32 * (PITCH_MAX - PITCH_MIN)
/ (SLOT_MAX - SLOT_MIN) as f32
+ PITCH_MIN;
client.send_message(format!("playing note with pitch: {}", pitch));
client.play_sound(
ident!("block.note_block.harp"),
SoundCategory::Block,
player.position(),
10.0,
pitch,
);
} else if clicked_slot == 44 {
client.set_game_mode(match client.game_mode() {
GameMode::Survival => GameMode::Creative,
GameMode::Creative => GameMode::Survival,
_ => GameMode::Creative,
});
}
}

View file

@ -40,6 +40,7 @@ use crate::protocol::packets::s2c::play::{
use crate::protocol::{BoundedInt, BoundedString, ByteAngle, NbtBridge, RawBytes, VarInt}; use crate::protocol::{BoundedInt, BoundedString, ByteAngle, NbtBridge, RawBytes, VarInt};
use crate::server::{C2sPacketChannels, NewClientData, S2cPlayMessage, SharedServer}; use crate::server::{C2sPacketChannels, NewClientData, S2cPlayMessage, SharedServer};
use crate::slab_versioned::{Key, VersionedSlab}; use crate::slab_versioned::{Key, VersionedSlab};
use crate::slot::Slot;
use crate::text::Text; use crate::text::Text;
use crate::util::{chunks_in_view_distance, is_chunk_in_view_distance}; use crate::util::{chunks_in_view_distance, is_chunk_in_view_distance};
use crate::world::{WorldId, Worlds}; use crate::world::{WorldId, Worlds};
@ -232,6 +233,9 @@ pub struct Client<C: Config> {
/// The data for the client's own player entity. /// The data for the client's own player entity.
player_data: Player, player_data: Player,
entity_events: Vec<entity::EntityEvent>, entity_events: Vec<entity::EntityEvent>,
/// The item currently being held by the client's cursor in an inventory
/// screen. Does not work for creative mode.
cursor_held_item: Slot,
} }
#[bitfield(u16)] #[bitfield(u16)]
@ -301,6 +305,7 @@ impl<C: Config> Client<C> {
.with_created_this_tick(true), .with_created_this_tick(true),
player_data: Player::new(), player_data: Player::new(),
entity_events: Vec::new(), entity_events: Vec::new(),
cursor_held_item: Slot::Empty,
} }
} }
@ -769,8 +774,39 @@ impl<C: Config> Client<C> {
} }
C2sPlayPacket::CommandSuggestionsRequest(_) => {} C2sPlayPacket::CommandSuggestionsRequest(_) => {}
C2sPlayPacket::ClickContainerButton(_) => {} C2sPlayPacket::ClickContainerButton(_) => {}
C2sPlayPacket::ClickContainer(_) => {} C2sPlayPacket::ClickContainer(p) => {
C2sPlayPacket::CloseContainerC2s(_) => {} if p.slot_idx == -999 {
// client is trying to drop the currently held stack
let held = std::mem::replace(&mut self.cursor_held_item, Slot::Empty);
match held {
Slot::Empty => {}
Slot::Present {
item_id,
item_count,
nbt,
} => self.events.push_back(ClientEvent::DropItemStack {
item_id,
item_count,
nbt,
}),
}
} 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::PluginMessageC2s(_) => {} C2sPlayPacket::PluginMessageC2s(_) => {}
C2sPlayPacket::EditBook(_) => {} C2sPlayPacket::EditBook(_) => {}
C2sPlayPacket::QueryEntityTag(_) => {} C2sPlayPacket::QueryEntityTag(_) => {}
@ -895,7 +931,7 @@ impl<C: Config> Client<C> {
face: p.face, face: p.face,
}, },
play::DiggingStatus::DropItemStack => return, play::DiggingStatus::DropItemStack => return,
play::DiggingStatus::DropItem => return, play::DiggingStatus::DropItem => ClientEvent::DropItem,
play::DiggingStatus::ShootArrowOrFinishEating => return, play::DiggingStatus::ShootArrowOrFinishEating => return,
play::DiggingStatus::SwapItemInHand => return, play::DiggingStatus::SwapItemInHand => return,
}); });
@ -929,7 +965,30 @@ impl<C: Config> Client<C> {
C2sPlayPacket::SetHeldItemS2c(_) => {} C2sPlayPacket::SetHeldItemS2c(_) => {}
C2sPlayPacket::ProgramCommandBlock(_) => {} C2sPlayPacket::ProgramCommandBlock(_) => {}
C2sPlayPacket::ProgramCommandBlockMinecart(_) => {} C2sPlayPacket::ProgramCommandBlockMinecart(_) => {}
C2sPlayPacket::SetCreativeModeSlot(_) => {} C2sPlayPacket::SetCreativeModeSlot(e) => {
if e.slot == -1 {
// The client is trying to drop a stack of items
match e.clicked_item {
Slot::Empty => log::warn!(
"Invalid packet, creative client tried to drop a stack of nothing."
),
Slot::Present {
item_id,
item_count,
nbt,
} => self.events.push_back(ClientEvent::DropItemStack {
item_id,
item_count,
nbt,
}),
}
} else {
self.events.push_back(ClientEvent::SetSlotCreative {
slot_id: e.slot,
slot: e.clicked_item,
})
}
}
C2sPlayPacket::ProgramJigsawBlock(_) => {} C2sPlayPacket::ProgramJigsawBlock(_) => {}
C2sPlayPacket::ProgramStructureBlock(_) => {} C2sPlayPacket::ProgramStructureBlock(_) => {}
C2sPlayPacket::UpdateSign(_) => {} C2sPlayPacket::UpdateSign(_) => {}

View file

@ -1,5 +1,6 @@
use std::time::Duration; use std::time::Duration;
use serde_nbt::Compound;
use vek::Vec3; use vek::Vec3;
use super::Client; use super::Client;
@ -7,11 +8,13 @@ use crate::block_pos::BlockPos;
use crate::config::Config; use crate::config::Config;
use crate::entity::types::Pose; use crate::entity::types::Pose;
use crate::entity::{Entity, EntityEvent, EntityId, TrackedData}; use crate::entity::{Entity, EntityEvent, EntityId, TrackedData};
use crate::protocol::packets::c2s::play::ClickContainerMode;
pub use crate::protocol::packets::c2s::play::{ pub use crate::protocol::packets::c2s::play::{
BlockFace, ChatMode, DisplayedSkinParts, Hand, MainHand, ResourcePackC2s as ResourcePackStatus, BlockFace, ChatMode, DisplayedSkinParts, Hand, MainHand, ResourcePackC2s as ResourcePackStatus,
}; };
pub use crate::protocol::packets::s2c::play::GameMode; pub use crate::protocol::packets::s2c::play::GameMode;
use crate::protocol::VarInt; use crate::protocol::VarInt;
use crate::slot::{Slot, SlotId};
/// Represents an action performed by a client. /// Represents an action performed by a client.
/// ///
@ -129,6 +132,49 @@ pub enum ClientEvent {
sequence: VarInt, sequence: VarInt,
}, },
ResourcePackStatusChanged(ResourcePackStatus), ResourcePackStatusChanged(ResourcePackStatus),
/// 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
item_id: VarInt,
item_count: u8,
nbt: Option<Compound>,
},
/// 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: Slot,
},
/// 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, Slot)>,
/// The item that is now being carried by the user's cursor
carried_item: Slot,
},
} }
#[derive(Clone, PartialEq, Debug)] #[derive(Clone, PartialEq, Debug)]
@ -287,6 +333,11 @@ pub fn handle_event_default<C: Config>(
ClientEvent::Digging { .. } => {} ClientEvent::Digging { .. } => {}
ClientEvent::InteractWithBlock { .. } => {} ClientEvent::InteractWithBlock { .. } => {}
ClientEvent::ResourcePackStatusChanged(_) => {} ClientEvent::ResourcePackStatusChanged(_) => {}
ClientEvent::CloseScreen { .. } => {}
ClientEvent::DropItem => {}
ClientEvent::DropItemStack { .. } => {}
ClientEvent::SetSlotCreative { .. } => {}
ClientEvent::ClickContainer { .. } => {}
} }
entity.set_world(client.world()); entity.set_world(client.world());

View file

@ -109,6 +109,7 @@ pub mod server;
mod slab; mod slab;
mod slab_rc; mod slab_rc;
mod slab_versioned; mod slab_versioned;
pub mod slot;
pub mod spatial_index; pub mod spatial_index;
pub mod text; pub mod text;
pub mod util; pub mod util;

View file

@ -12,7 +12,6 @@ pub use byte_angle::ByteAngle;
use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
use serde::de::DeserializeOwned; use serde::de::DeserializeOwned;
use serde::Serialize; use serde::Serialize;
pub use slot::Slot;
use uuid::Uuid; use uuid::Uuid;
pub use var_int::VarInt; pub use var_int::VarInt;
pub use var_long::VarLong; pub use var_long::VarLong;
@ -24,7 +23,6 @@ use crate::nbt;
mod byte_angle; mod byte_angle;
pub mod codec; pub mod codec;
pub mod packets; pub mod packets;
mod slot;
mod var_int; mod var_int;
mod var_long; mod var_long;

View file

@ -20,9 +20,10 @@ use crate::block_pos::BlockPos;
use crate::ident::Ident; use crate::ident::Ident;
use crate::nbt::Compound; use crate::nbt::Compound;
use crate::protocol::{ use crate::protocol::{
BoundedArray, BoundedInt, BoundedString, ByteAngle, Decode, Encode, NbtBridge, RawBytes, Slot, BoundedArray, BoundedInt, BoundedString, ByteAngle, Decode, Encode, NbtBridge, RawBytes,
VarInt, VarLong, VarInt, VarLong,
}; };
use crate::slot::Slot;
use crate::text::Text; use crate::text::Text;
/// Provides the name of a packet for debugging purposes. /// Provides the name of a packet for debugging purposes.

View file

@ -255,10 +255,13 @@ pub mod play {
slot_idx: i16, slot_idx: i16,
button: i8, button: i8,
mode: ClickContainerMode, mode: ClickContainerMode,
slots: Vec<(i16, Slot)>,
carried_item: Slot,
} }
} }
def_enum! { def_enum! {
#[derive(Copy, PartialEq, Eq)]
ClickContainerMode: VarInt { ClickContainerMode: VarInt {
Click = 0, Click = 0,
ShiftClick = 1, ShiftClick = 1,

View file

@ -5,6 +5,8 @@ use byteorder::ReadBytesExt;
use crate::nbt::Compound; use crate::nbt::Compound;
use crate::protocol::{Decode, Encode, VarInt}; use crate::protocol::{Decode, Encode, VarInt};
pub type SlotId = i16;
/// Represents a slot in an inventory. /// Represents a slot in an inventory.
#[derive(Clone, Default, Debug)] #[derive(Clone, Default, Debug)]
pub enum Slot { pub enum Slot {