From b604dafe73a7450257912b37cd495152ed58fb4a Mon Sep 17 00:00:00 2001 From: Ryan Date: Sun, 17 Jul 2022 21:29:44 -0700 Subject: [PATCH] Add combat example --- build/entity.rs | 2 +- examples/combat.rs | 278 ++++++++++++++++++++++++++++++++++++++++++++ examples/conway.rs | 67 ++++------- src/client.rs | 84 ++++++++++--- src/client/event.rs | 23 +++- src/config.rs | 6 +- src/entity.rs | 3 +- src/lib.rs | 10 +- 8 files changed, 403 insertions(+), 70 deletions(-) create mode 100644 examples/combat.rs diff --git a/build/entity.rs b/build/entity.rs index 8e2f0e7..8d48f4a 100644 --- a/build/entity.rs +++ b/build/entity.rs @@ -455,7 +455,7 @@ const LIVING_ENTITY: Class = Class { typ: Type::OptBlockPos(None), }, ], - events: &[], + events: &[Event::Hurt], }; const MOB: Class = Class { diff --git a/examples/combat.rs b/examples/combat.rs new file mode 100644 index 0000000..e2f6f88 --- /dev/null +++ b/examples/combat.rs @@ -0,0 +1,278 @@ +use std::net::SocketAddr; +use std::sync::atomic::{AtomicUsize, Ordering}; + +use log::LevelFilter; +use valence::block::{BlockPos, BlockState}; +use valence::client::Event::{self}; +use valence::client::{ClientId, GameMode, Hand, InteractWithEntityKind}; +use valence::config::{Config, ServerListPing}; +use valence::dimension::DimensionId; +use valence::entity::state::Pose; +use valence::entity::{EntityId, EntityKind, EntityState}; +use valence::server::{Server, SharedServer, ShutdownResult}; +use valence::text::{Color, TextFormat}; +use valence::{async_trait, Ticks}; +use vek::Vec3; + +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), + }, + (), + ) +} + +struct Game { + player_count: AtomicUsize, +} + +#[derive(Default)] +struct ClientData { + /// The client's player entity. + player: EntityId, + /// The extra knockback on the first hit while sprinting. + has_extra_knockback: bool, +} + +#[derive(Default)] +struct EntityData { + client: ClientId, + attacked: bool, + attacker_pos: Vec3, + extra_knockback: bool, + last_attack_time: Ticks, +} + +const MAX_PLAYERS: usize = 10; + +const SPAWN_POS: BlockPos = BlockPos::new(0, 20, 0); + +#[async_trait] +impl Config for Game { + type ChunkData = (); + type ClientData = ClientData; + type EntityData = EntityData; + type ServerData = (); + type WorldData = (); + + fn max_connections(&self) -> usize { + // We want status pings to be successful even if the server is full. + MAX_PLAYERS + 64 + } + + fn online_mode(&self) -> bool { + // You'll want this to be true on real servers. + false + } + + async fn server_list_ping( + &self, + _server: &SharedServer, + _remote_addr: SocketAddr, + ) -> ServerListPing { + ServerListPing::Respond { + online_players: self.player_count.load(Ordering::SeqCst) as i32, + max_players: MAX_PLAYERS as i32, + description: "Hello Valence!".color(Color::AQUA), + favicon_png: Some(include_bytes!("../assets/favicon.png")), + } + } + + fn init(&self, server: &mut Server) { + let (_, world) = server.worlds.create(DimensionId::default(), ()); + world.meta.set_flat(true); + + let min_y = server.shared.dimension(DimensionId::default()).min_y; + + // Create circular arena. + let size = 2; + for chunk_z in -size - 2..size + 2 { + for chunk_x in -size - 2..size + 2 { + let chunk = world.chunks.create([chunk_x, chunk_z], ()); + let r = -size..size; + if r.contains(&chunk_x) && r.contains(&chunk_z) { + for z in 0..16 { + for x in 0..16 { + let block_x = chunk_x * 16 + x as i32; + let block_z = chunk_z * 16 + z as i32; + if f64::hypot(block_x as f64, block_z as f64) <= size as f64 * 16.0 { + for y in 0..(SPAWN_POS.y - min_y + 1) as usize { + chunk.set_block_state(x, y, z, BlockState::STONE); + } + } + } + } + } + } + } + + world.chunks.set_block_state(SPAWN_POS, BlockState::BEDROCK); + } + + fn update(&self, server: &mut Server) { + let (world_id, world) = server.worlds.iter_mut().next().unwrap(); + + let current_tick = server.shared.current_tick(); + + server.clients.retain(|client_id, client| { + if client.created_tick() == current_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; + } + + client.spawn(world_id); + client.set_game_mode(GameMode::Survival); + client.teleport( + [ + SPAWN_POS.x as f64 + 0.5, + SPAWN_POS.y as f64 + 1.0, + SPAWN_POS.z as f64 + 0.5, + ], + 0.0, + 0.0, + ); + + world.meta.player_list_mut().insert( + client.uuid(), + client.username().to_owned(), + client.textures().cloned(), + client.game_mode(), + 0, + None, + ); + + let (player_id, player) = server + .entities + .create_with_uuid(EntityKind::Player, client.uuid(), EntityData::default()) + .unwrap(); + + client.data.player = player_id; + client.data.has_extra_knockback = true; + + player.data.client = client_id; + player.data.last_attack_time = 0; + + client.send_message("Welcome to the arena.".italic()); + if self.player_count.load(Ordering::SeqCst) <= 1 { + client.send_message("Have another player join the game with you.".italic()); + } + } + + if client.is_disconnected() { + self.player_count.fetch_sub(1, Ordering::SeqCst); + server.entities.delete(client.data.player); + world.meta.player_list_mut().remove(client.uuid()); + return false; + } + + while let Some(event) = client.pop_event() { + match event { + Event::StartSprinting => { + client.data.has_extra_knockback = true; + } + Event::InteractWithEntity { + id, + kind: InteractWithEntityKind::Attack, + .. + } => { + if let Some(target) = server.entities.get_mut(id) { + if !target.data.attacked + && current_tick - target.data.last_attack_time >= 10 + && id != client.data.player + { + target.data.attacked = true; + target.data.attacker_pos = client.position(); + target.data.extra_knockback = client.data.has_extra_knockback; + target.data.last_attack_time = current_tick; + + client.data.has_extra_knockback = false; + } + } + } + Event::ArmSwing(hand) => { + let player = server.entities.get_mut(client.data.player).unwrap(); + + if let EntityState::Player(e) = &mut player.state { + match hand { + Hand::Main => e.trigger_swing_main_arm(), + Hand::Off => e.trigger_swing_offhand(), + } + } + } + _ => (), + } + } + + if client.position().y <= 0.0 { + client.teleport( + [ + SPAWN_POS.x as f64 + 0.5, + SPAWN_POS.y as f64 + 1.0, + SPAWN_POS.z as f64 + 0.5, + ], + client.yaw(), + client.pitch(), + ); + } + + let player = server.entities.get_mut(client.data.player).unwrap(); + + player.set_world(client.world()); + player.set_position(client.position()); + player.set_yaw(client.yaw()); + player.set_head_yaw(client.yaw()); + player.set_pitch(client.pitch()); + player.set_on_ground(client.on_ground()); + + if let EntityState::Player(player) = &mut player.state { + if client.is_sneaking() { + player.set_pose(Pose::Sneaking); + } else { + player.set_pose(Pose::Standing); + } + + player.set_sprinting(client.is_sprinting()); + } + + true + }); + + for (_, e) in server.entities.iter_mut() { + if e.data.attacked { + e.data.attacked = false; + let victim = server.clients.get_mut(e.data.client).unwrap(); + + let mut vel = (victim.position() - e.data.attacker_pos).normalized(); + + let knockback_xz = if e.data.extra_knockback { 18.0 } else { 8.0 }; + let knockback_y = if e.data.extra_knockback { 8.432 } else { 6.432 }; + + vel.x *= knockback_xz; + vel.y = knockback_y; + vel.z *= knockback_xz; + + victim.set_velocity(victim.velocity() / 2.0 + vel.as_()); + + if let EntityState::Player(e) = &mut e.state { + e.trigger_take_damage(); + e.trigger_hurt(); + } + victim.player_mut().trigger_take_damage(); + victim.player_mut().trigger_hurt(); + } + } + } +} diff --git a/examples/conway.rs b/examples/conway.rs index d17c0bd..0efa105 100644 --- a/examples/conway.rs +++ b/examples/conway.rs @@ -145,14 +145,12 @@ impl Config for Game { None, ); - let player_id = server + client.data.player = server .entities .create_with_uuid(EntityKind::Player, client.uuid(), ()) .unwrap() .0; - client.data = ClientData { player: player_id }; - client.send_message("Welcome to Conway's game of life in Minecraft!".italic()); client.send_message("Hold the left mouse button to bring blocks to life.".italic()); } @@ -164,12 +162,12 @@ impl Config for Game { return false; } - true - }); - - for (_, client) in server.clients.iter_mut() { let player = server.entities.get_mut(client.data.player).unwrap(); + if client.position().y <= 0.0 { + client.teleport(spawn_pos, client.yaw(), client.pitch()); + } + while let Some(event) = client.pop_event() { match event { Event::Digging { position, .. } => { @@ -181,40 +179,6 @@ impl Config for Game { true; } } - Event::Movement { .. } => { - if client.position().y <= 0.0 { - client.teleport(spawn_pos, client.yaw(), client.pitch()); - } - - player.set_world(client.world()); - player.set_position(client.position()); - player.set_yaw(client.yaw()); - player.set_head_yaw(client.yaw()); - player.set_pitch(client.pitch()); - player.set_on_ground(client.on_ground()); - } - Event::StartSneaking => { - if let EntityState::Player(e) = &mut player.state { - e.set_crouching(true); - e.set_pose(Pose::Sneaking); - } - } - Event::StopSneaking => { - if let EntityState::Player(e) = &mut player.state { - e.set_pose(Pose::Standing); - e.set_crouching(false); - } - } - Event::StartSprinting => { - if let EntityState::Player(e) = &mut player.state { - e.set_sprinting(true); - } - } - Event::StopSprinting => { - if let EntityState::Player(e) = &mut player.state { - e.set_sprinting(false); - } - } Event::ArmSwing(hand) => { if let EntityState::Player(e) = &mut player.state { match hand { @@ -226,7 +190,26 @@ impl Config for Game { _ => {} } } - } + + player.set_world(client.world()); + player.set_position(client.position()); + player.set_yaw(client.yaw()); + player.set_head_yaw(client.yaw()); + player.set_pitch(client.pitch()); + player.set_on_ground(client.on_ground()); + + if let EntityState::Player(player) = &mut player.state { + if client.is_sneaking() { + player.set_pose(Pose::Sneaking); + } else { + player.set_pose(Pose::Standing); + } + + player.set_sprinting(client.is_sprinting()); + } + + true + }); if server.shared.current_tick() % 4 != 0 { return; diff --git a/src/client.rs b/src/client.rs index c0a1f77..e3789b2 100644 --- a/src/client.rs +++ b/src/client.rs @@ -41,7 +41,7 @@ use crate::slotmap::{Key, SlotMap}; use crate::text::Text; use crate::util::{chunks_in_view_distance, is_chunk_in_view_distance}; use crate::world::{WorldId, Worlds}; -use crate::{ident, Ticks, LIBRARY_NAMESPACE}; +use crate::{ident, Ticks, LIBRARY_NAMESPACE, STANDARD_TPS}; /// A container for all [`Client`]s on a [`Server`](crate::server::Server). /// @@ -175,6 +175,8 @@ pub struct Client { world: WorldId, new_position: Vec3, old_position: Vec3, + /// Measured in m/s. + velocity: Vec3, /// Measured in degrees yaw: f32, /// Measured in degrees @@ -228,7 +230,8 @@ pub(crate) struct ClientFlags { hardcore: bool, attack_speed_modified: bool, movement_speed_modified: bool, - #[bits(5)] + velocity_modified: bool, + #[bits(4)] _pad: u8, } @@ -252,6 +255,7 @@ impl Client { world: WorldId::default(), new_position: Vec3::default(), old_position: Vec3::default(), + velocity: Vec3::default(), yaw: 0.0, pitch: 0.0, teleport_id_counter: 0, @@ -294,6 +298,16 @@ impl Client { &self.username } + /// Returns the sneaking state of this client. + pub fn is_sneaking(&self) -> bool { + self.flags.sneaking() + } + + /// Returns the sprinting state of this client. + pub fn is_sprinting(&self) -> bool { + self.flags.sprinting() + } + /// Gets the player textures of this client. If the client does not have /// a skin, then `None` is returned. pub fn textures(&self) -> Option<&SignedPlayerTextures> { @@ -333,9 +347,9 @@ impl Client { /// If you want to change the client's world, use [`Self::spawn`]. pub fn teleport(&mut self, pos: impl Into>, yaw: f32, pitch: f32) { self.new_position = pos.into(); - self.yaw = yaw; self.pitch = pitch; + self.velocity = Vec3::default(); if !self.flags.teleported_this_tick() { self.flags.set_teleported_this_tick(true); @@ -353,11 +367,18 @@ impl Client { } } + /// Gets the velocity of this client in m/s. + /// + /// The velocity of a client is derived from their current and previous + /// position. + pub fn velocity(&self) -> Vec3 { + self.velocity + } + + /// Sets the client's velocity in m/s. pub fn set_velocity(&mut self, velocity: impl Into>) { - self.send_packet(SetEntityMotion { - entity_id: VarInt(0), - velocity: velocity_to_packet_units(velocity.into()), - }); + self.velocity = velocity.into(); + self.flags.set_velocity_modified(true); } /// Gets this client's yaw. @@ -610,14 +631,25 @@ impl Client { if client.pending_teleports == 0 { // TODO: validate movement using swept AABB collision with the blocks. // TODO: validate that the client is actually inside/outside the vehicle? + + // Movement packets should be coming in at a rate of STANDARD_TPS. + let new_velocity = (new_position - client.new_position).as_() * STANDARD_TPS as f32; + let event = Event::Movement { - position: client.new_position, - yaw: client.yaw, - pitch: client.pitch, - on_ground: client.flags.on_ground(), + old_position: client.new_position, + old_velocity: client.velocity, + old_yaw: client.yaw, + old_pitch: client.pitch, + old_on_ground: client.flags.on_ground(), + new_position, + new_velocity, + new_yaw, + new_pitch, + new_on_ground, }; client.new_position = new_position; + client.velocity = new_velocity; client.yaw = new_yaw; client.pitch = new_pitch; client.flags.set_on_ground(new_on_ground); @@ -787,21 +819,34 @@ impl Client { // - Can't jump with a horse if not on a horse // - Can't open horse inventory if not on a horse. // - Can't fly with elytra if not wearing an elytra. + // - Can't jump with horse while already jumping & vice versa? self.events.push_back(match e.action_id { PlayerCommandId::StartSneaking => { + if self.flags.sneaking() { + return; + } self.flags.set_sneaking(true); Event::StartSneaking } PlayerCommandId::StopSneaking => { + if !self.flags.sneaking() { + return; + } self.flags.set_sneaking(false); Event::StopSneaking } PlayerCommandId::LeaveBed => Event::LeaveBed, PlayerCommandId::StartSprinting => { + if self.flags.sprinting() { + return; + } self.flags.set_sprinting(true); Event::StartSprinting } PlayerCommandId::StopSprinting => { + if !self.flags.sprinting() { + return; + } self.flags.set_sprinting(false); Event::StopSprinting } @@ -914,6 +959,7 @@ impl Client { self.teleport(self.position(), self.yaw(), self.pitch()); } else { if self.flags.spawn() { + self.flags.set_spawn(false); self.loaded_entities.clear(); self.loaded_chunks.clear(); @@ -1117,6 +1163,17 @@ impl Client { }); } + // Set velocity. Do this after teleporting since teleporting sets velocity to + // zero. + if self.flags.velocity_modified() { + self.flags.set_velocity_modified(false); + + self.send_packet(SetEntityMotion { + entity_id: VarInt(0), + velocity: velocity_to_packet_units(self.velocity), + }); + } + // Send chat messages. for msg in self.msgs_to_send.drain(..) { send_packet( @@ -1244,7 +1301,6 @@ impl Client { metadata: RawBytes(data), }); } - self.player_data.clear_modifications(); // Spawn new entities within the view distance. let pos = self.position(); @@ -1253,7 +1309,7 @@ impl Client { |id, _| { let entity = entities .get(id) - .expect("entities in spatial index should be valid"); + .expect("entity IDs in spatial index should be valid at this point"); if entity.kind() != EntityKind::Marker && entity.uuid() != self.uuid && self.loaded_entities.insert(id) @@ -1288,8 +1344,8 @@ impl Client { // any effect. } + self.player_data.clear_modifications(); self.old_position = self.new_position; - self.flags.set_spawn(false); } } diff --git a/src/client/event.rs b/src/client/event.rs index 537cee9..badff92 100644 --- a/src/client/event.rs +++ b/src/client/event.rs @@ -24,17 +24,28 @@ pub enum Event { /// Settings were changed. The value in this variant is the _previous_ /// client settings. SettingsChanged(Option), - /// The client moved. The values in this - /// variant are the _previous_ position and look. + /// The client moved. Movement { /// Absolute coordinates of the previous position. - position: Vec3, + old_position: Vec3, + /// Previous velocity in m/s. + old_velocity: Vec3, /// The previous yaw (in degrees). - yaw: f32, + old_yaw: f32, /// The previous pitch (in degrees). - pitch: f32, + old_pitch: f32, /// If the client was previously on the ground. - on_ground: bool, + old_on_ground: bool, + /// Absolute coodinates of the new position. + new_position: Vec3, + /// New velocity in m/s. + new_velocity: Vec3, + /// The new yaw (in degrees). + new_yaw: f32, + /// The new pitch (in degrees). + new_pitch: f32, + /// If the client is now on the ground. + new_on_ground: bool, }, StartSneaking, StopSneaking, diff --git a/src/config.rs b/src/config.rs index 57f8cd6..6daba09 100644 --- a/src/config.rs +++ b/src/config.rs @@ -10,7 +10,7 @@ use crate::biome::Biome; use crate::dimension::Dimension; use crate::server::{NewClientData, Server, SharedServer}; use crate::text::Text; -use crate::Ticks; +use crate::{Ticks, STANDARD_TPS}; /// A trait for the configuration of a server. /// @@ -65,9 +65,9 @@ pub trait Config: 'static + Sized + Send + Sync + UnwindSafe + RefUnwindSafe { /// /// # Default Implementation /// - /// Returns `20`, which is the same as Minecraft's official server. + /// Returns [`STANDARD_TPS`]. fn tick_rate(&self) -> Ticks { - 20 + STANDARD_TPS } /// Called once at startup to get the "online mode" option, which determines diff --git a/src/entity.rs b/src/entity.rs index 60c57d6..318403c 100644 --- a/src/entity.rs +++ b/src/entity.rs @@ -22,6 +22,7 @@ use crate::protocol_inner::{ByteAngle, RawBytes, VarInt}; use crate::slotmap::{Key, SlotMap}; use crate::util::aabb_from_bottom_and_size; use crate::world::WorldId; +use crate::STANDARD_TPS; /// A container for all [`Entity`]s on a [`Server`](crate::server::Server). /// @@ -583,7 +584,7 @@ impl Entity { pub(crate) fn velocity_to_packet_units(vel: Vec3) -> Vec3 { // The saturating cast to i16 is desirable. - (vel * 400.0).as_() + (8000.0 / STANDARD_TPS as f32 * vel).as_() } pub(crate) enum EntitySpawnPacket { diff --git a/src/lib.rs b/src/lib.rs index 451978b..439213a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -168,11 +168,15 @@ pub const VERSION_NAME: &str = "1.19"; /// [identifiers](crate::ident::Ident). /// /// You should avoid using this namespace in your own identifiers. -const LIBRARY_NAMESPACE: &str = "valence"; +pub const LIBRARY_NAMESPACE: &str = "valence"; /// A discrete unit of time where 1 tick is the duration of a /// single game update. /// -/// The duration of a game update depends on the current configuration, which -/// may or may not be the same as Minecraft's standard 20 ticks/second. +/// The duration of a game update on a Valence server depends on the current +/// configuration. In some contexts, "ticks" refer to the configured tick rate +/// while others refer to Minecraft's [standard TPS](STANDARD_TPS). pub type Ticks = i64; + +/// Minecraft's standard ticks per second (TPS). +pub const STANDARD_TPS: Ticks = 20;