From f165b55c7b8ff11ce21b1808680e780afb1d3fcb Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Tue, 13 Sep 2022 21:30:45 -0400 Subject: [PATCH] add building example (#49) This adds a new example to demonstrate how to implement block breaking/placing. --- examples/building.rs | 232 +++++++++++++++++++++++++++++++++++++++++++ src/block_pos.rs | 23 +++++ src/client.rs | 18 ++-- 3 files changed, 263 insertions(+), 10 deletions(-) create mode 100644 examples/building.rs diff --git a/examples/building.rs b/examples/building.rs new file mode 100644 index 0000000..ce792e9 --- /dev/null +++ b/examples/building.rs @@ -0,0 +1,232 @@ +use std::net::SocketAddr; +use std::sync::atomic::{AtomicUsize, Ordering}; + +use log::LevelFilter; +use num::Integer; +use valence::biome::Biome; +use valence::block::BlockState; +use valence::chunk::{Chunk, UnloadedChunk}; +use valence::client::{handle_event_default, ClientEvent, DiggingStatus, GameMode, Hand}; +use valence::config::{Config, ServerListPing}; +use valence::dimension::{Dimension, DimensionId}; +use valence::entity::{EntityId, EntityKind}; +use valence::player_list::PlayerListId; +use valence::server::{Server, SharedServer, ShutdownResult}; +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, +} + +#[derive(Default)] +struct ClientState { + entity_id: EntityId, +} + +const MAX_PLAYERS: usize = 10; + +const SIZE_X: usize = 100; +const SIZE_Z: usize = 100; + +#[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 { + vec![Dimension { + fixed_time: Some(6000), + ..Dimension::default() + }] + } + + fn biomes(&self) -> Vec { + vec![Biome { + name: ident!("valence:default_biome"), + grass_color: Some(0x00ff00), + ..Biome::default() + }] + } + + async fn server_list_ping( + &self, + _server: &SharedServer, + _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) { + 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) { + let (world_id, world) = 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! 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.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::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::InteractWithBlock { + hand, + location, + face, + .. + } => { + if hand == Hand::Main { + let place_at = location.get_in_direction(face); + // TODO: get block from player's inventory slot + world.chunks.set_block_state(place_at, BlockState::DIRT); + } + } + _ => {} + } + } + + true + }); + } +} diff --git a/src/block_pos.rs b/src/block_pos.rs index 0ae69e8..7c005ec 100644 --- a/src/block_pos.rs +++ b/src/block_pos.rs @@ -3,6 +3,7 @@ use std::io::Write; use anyhow::bail; use vek::Vec3; +use crate::client::BlockFace; use crate::protocol::{Decode, Encode}; /// Represents an absolute block position in world space. @@ -23,6 +24,28 @@ impl BlockPos { pub fn at(pos: impl Into>) -> Self { pos.into().floor().as_::().into() } + + /// Get a new [`BlockPos`] that is adjacent to this position in `dir` + /// direction. + /// + /// ```rust + /// use valence::block::BlockPos; + /// use valence::client::BlockFace; + /// + /// let pos = BlockPos::new(0, 0, 0); + /// let adj = pos.get_in_direction(BlockFace::South); + /// assert_eq!(adj, BlockPos::new(0, 0, 1)); + /// ``` + pub fn get_in_direction(self, dir: BlockFace) -> BlockPos { + match dir { + BlockFace::Bottom => BlockPos::new(self.x, self.y - 1, self.z), + BlockFace::Top => BlockPos::new(self.x, self.y + 1, self.z), + BlockFace::North => BlockPos::new(self.x, self.y, self.z - 1), + BlockFace::South => BlockPos::new(self.x, self.y, self.z + 1), + BlockFace::West => BlockPos::new(self.x - 1, self.y, self.z), + BlockFace::East => BlockPos::new(self.x + 1, self.y, self.z), + } + } } impl Encode for BlockPos { diff --git a/src/client.rs b/src/client.rs index e3ebc87..e2f7aa7 100644 --- a/src/client.rs +++ b/src/client.rs @@ -24,9 +24,7 @@ use crate::entity::{ use crate::ident::Ident; use crate::player_list::{PlayerListId, PlayerLists}; use crate::player_textures::SignedPlayerTextures; -use crate::protocol::packets::c2s::play::{ - C2sPlayPacket, DiggingStatus, InteractKind, PlayerCommandId, -}; +use crate::protocol::packets::c2s::play::{self, C2sPlayPacket, InteractKind, PlayerCommandId}; pub use crate::protocol::packets::s2c::play::TitleFade; use crate::protocol::packets::s2c::play::{ BiomeRegistry, ChatTypeRegistry, ChunkLoadDistance, ChunkRenderDistanceCenter, ClearTitles, @@ -853,25 +851,25 @@ impl Client { } self.events.push_back(match p.status { - DiggingStatus::StartedDigging => ClientEvent::Digging { + play::DiggingStatus::StartedDigging => ClientEvent::Digging { status: event::DiggingStatus::Start, position: p.location, face: p.face, }, - DiggingStatus::CancelledDigging => ClientEvent::Digging { + play::DiggingStatus::CancelledDigging => ClientEvent::Digging { status: event::DiggingStatus::Cancel, position: p.location, face: p.face, }, - DiggingStatus::FinishedDigging => ClientEvent::Digging { + play::DiggingStatus::FinishedDigging => ClientEvent::Digging { status: event::DiggingStatus::Finish, position: p.location, face: p.face, }, - DiggingStatus::DropItemStack => return, - DiggingStatus::DropItem => return, - DiggingStatus::ShootArrowOrFinishEating => return, - DiggingStatus::SwapItemInHand => return, + play::DiggingStatus::DropItemStack => return, + play::DiggingStatus::DropItem => return, + play::DiggingStatus::ShootArrowOrFinishEating => return, + play::DiggingStatus::SwapItemInHand => return, }); } C2sPlayPacket::PlayerCommand(c) => {