Add combat example

This commit is contained in:
Ryan 2022-07-17 21:29:44 -07:00
parent 865ab76699
commit b604dafe73
8 changed files with 403 additions and 70 deletions

View file

@ -455,7 +455,7 @@ const LIVING_ENTITY: Class = Class {
typ: Type::OptBlockPos(None), typ: Type::OptBlockPos(None),
}, },
], ],
events: &[], events: &[Event::Hurt],
}; };
const MOB: Class = Class { const MOB: Class = Class {

278
examples/combat.rs Normal file
View file

@ -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<f64>,
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<Self>,
_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<Self>) {
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<Self>) {
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();
}
}
}
}

View file

@ -145,14 +145,12 @@ impl Config for Game {
None, None,
); );
let player_id = server client.data.player = server
.entities .entities
.create_with_uuid(EntityKind::Player, client.uuid(), ()) .create_with_uuid(EntityKind::Player, client.uuid(), ())
.unwrap() .unwrap()
.0; .0;
client.data = ClientData { player: player_id };
client.send_message("Welcome to Conway's game of life in Minecraft!".italic()); 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()); client.send_message("Hold the left mouse button to bring blocks to life.".italic());
} }
@ -164,12 +162,12 @@ impl Config for Game {
return false; return false;
} }
true
});
for (_, client) in server.clients.iter_mut() {
let player = server.entities.get_mut(client.data.player).unwrap(); 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() { while let Some(event) = client.pop_event() {
match event { match event {
Event::Digging { position, .. } => { Event::Digging { position, .. } => {
@ -181,40 +179,6 @@ impl Config for Game {
true; 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) => { Event::ArmSwing(hand) => {
if let EntityState::Player(e) = &mut player.state { if let EntityState::Player(e) = &mut player.state {
match hand { match hand {
@ -226,8 +190,27 @@ 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 { if server.shared.current_tick() % 4 != 0 {
return; return;
} }

View file

@ -41,7 +41,7 @@ use crate::slotmap::{Key, SlotMap};
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};
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). /// A container for all [`Client`]s on a [`Server`](crate::server::Server).
/// ///
@ -175,6 +175,8 @@ pub struct Client<C: Config> {
world: WorldId, world: WorldId,
new_position: Vec3<f64>, new_position: Vec3<f64>,
old_position: Vec3<f64>, old_position: Vec3<f64>,
/// Measured in m/s.
velocity: Vec3<f32>,
/// Measured in degrees /// Measured in degrees
yaw: f32, yaw: f32,
/// Measured in degrees /// Measured in degrees
@ -228,7 +230,8 @@ pub(crate) struct ClientFlags {
hardcore: bool, hardcore: bool,
attack_speed_modified: bool, attack_speed_modified: bool,
movement_speed_modified: bool, movement_speed_modified: bool,
#[bits(5)] velocity_modified: bool,
#[bits(4)]
_pad: u8, _pad: u8,
} }
@ -252,6 +255,7 @@ impl<C: Config> Client<C> {
world: WorldId::default(), world: WorldId::default(),
new_position: Vec3::default(), new_position: Vec3::default(),
old_position: Vec3::default(), old_position: Vec3::default(),
velocity: Vec3::default(),
yaw: 0.0, yaw: 0.0,
pitch: 0.0, pitch: 0.0,
teleport_id_counter: 0, teleport_id_counter: 0,
@ -294,6 +298,16 @@ impl<C: Config> Client<C> {
&self.username &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 /// Gets the player textures of this client. If the client does not have
/// a skin, then `None` is returned. /// a skin, then `None` is returned.
pub fn textures(&self) -> Option<&SignedPlayerTextures> { pub fn textures(&self) -> Option<&SignedPlayerTextures> {
@ -333,9 +347,9 @@ impl<C: Config> Client<C> {
/// If you want to change the client's world, use [`Self::spawn`]. /// If you want to change the client's world, use [`Self::spawn`].
pub fn teleport(&mut self, pos: impl Into<Vec3<f64>>, yaw: f32, pitch: f32) { pub fn teleport(&mut self, pos: impl Into<Vec3<f64>>, yaw: f32, pitch: f32) {
self.new_position = pos.into(); self.new_position = pos.into();
self.yaw = yaw; self.yaw = yaw;
self.pitch = pitch; self.pitch = pitch;
self.velocity = Vec3::default();
if !self.flags.teleported_this_tick() { if !self.flags.teleported_this_tick() {
self.flags.set_teleported_this_tick(true); self.flags.set_teleported_this_tick(true);
@ -353,11 +367,18 @@ impl<C: Config> Client<C> {
} }
} }
/// 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<f32> {
self.velocity
}
/// Sets the client's velocity in m/s.
pub fn set_velocity(&mut self, velocity: impl Into<Vec3<f32>>) { pub fn set_velocity(&mut self, velocity: impl Into<Vec3<f32>>) {
self.send_packet(SetEntityMotion { self.velocity = velocity.into();
entity_id: VarInt(0), self.flags.set_velocity_modified(true);
velocity: velocity_to_packet_units(velocity.into()),
});
} }
/// Gets this client's yaw. /// Gets this client's yaw.
@ -610,14 +631,25 @@ impl<C: Config> Client<C> {
if client.pending_teleports == 0 { if client.pending_teleports == 0 {
// TODO: validate movement using swept AABB collision with the blocks. // TODO: validate movement using swept AABB collision with the blocks.
// TODO: validate that the client is actually inside/outside the vehicle? // 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 { let event = Event::Movement {
position: client.new_position, old_position: client.new_position,
yaw: client.yaw, old_velocity: client.velocity,
pitch: client.pitch, old_yaw: client.yaw,
on_ground: client.flags.on_ground(), 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.new_position = new_position;
client.velocity = new_velocity;
client.yaw = new_yaw; client.yaw = new_yaw;
client.pitch = new_pitch; client.pitch = new_pitch;
client.flags.set_on_ground(new_on_ground); client.flags.set_on_ground(new_on_ground);
@ -787,21 +819,34 @@ impl<C: Config> Client<C> {
// - Can't jump with a horse if not on a horse // - Can't jump with a horse if not on a horse
// - Can't open horse inventory 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 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 { self.events.push_back(match e.action_id {
PlayerCommandId::StartSneaking => { PlayerCommandId::StartSneaking => {
if self.flags.sneaking() {
return;
}
self.flags.set_sneaking(true); self.flags.set_sneaking(true);
Event::StartSneaking Event::StartSneaking
} }
PlayerCommandId::StopSneaking => { PlayerCommandId::StopSneaking => {
if !self.flags.sneaking() {
return;
}
self.flags.set_sneaking(false); self.flags.set_sneaking(false);
Event::StopSneaking Event::StopSneaking
} }
PlayerCommandId::LeaveBed => Event::LeaveBed, PlayerCommandId::LeaveBed => Event::LeaveBed,
PlayerCommandId::StartSprinting => { PlayerCommandId::StartSprinting => {
if self.flags.sprinting() {
return;
}
self.flags.set_sprinting(true); self.flags.set_sprinting(true);
Event::StartSprinting Event::StartSprinting
} }
PlayerCommandId::StopSprinting => { PlayerCommandId::StopSprinting => {
if !self.flags.sprinting() {
return;
}
self.flags.set_sprinting(false); self.flags.set_sprinting(false);
Event::StopSprinting Event::StopSprinting
} }
@ -914,6 +959,7 @@ impl<C: Config> Client<C> {
self.teleport(self.position(), self.yaw(), self.pitch()); self.teleport(self.position(), self.yaw(), self.pitch());
} else { } else {
if self.flags.spawn() { if self.flags.spawn() {
self.flags.set_spawn(false);
self.loaded_entities.clear(); self.loaded_entities.clear();
self.loaded_chunks.clear(); self.loaded_chunks.clear();
@ -1117,6 +1163,17 @@ impl<C: Config> Client<C> {
}); });
} }
// 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. // Send chat messages.
for msg in self.msgs_to_send.drain(..) { for msg in self.msgs_to_send.drain(..) {
send_packet( send_packet(
@ -1244,7 +1301,6 @@ impl<C: Config> Client<C> {
metadata: RawBytes(data), metadata: RawBytes(data),
}); });
} }
self.player_data.clear_modifications();
// Spawn new entities within the view distance. // Spawn new entities within the view distance.
let pos = self.position(); let pos = self.position();
@ -1253,7 +1309,7 @@ impl<C: Config> Client<C> {
|id, _| { |id, _| {
let entity = entities let entity = entities
.get(id) .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 if entity.kind() != EntityKind::Marker
&& entity.uuid() != self.uuid && entity.uuid() != self.uuid
&& self.loaded_entities.insert(id) && self.loaded_entities.insert(id)
@ -1288,8 +1344,8 @@ impl<C: Config> Client<C> {
// any effect. // any effect.
} }
self.player_data.clear_modifications();
self.old_position = self.new_position; self.old_position = self.new_position;
self.flags.set_spawn(false);
} }
} }

View file

@ -24,17 +24,28 @@ pub enum Event {
/// Settings were changed. The value in this variant is the _previous_ /// Settings were changed. The value in this variant is the _previous_
/// client settings. /// client settings.
SettingsChanged(Option<Settings>), SettingsChanged(Option<Settings>),
/// The client moved. The values in this /// The client moved.
/// variant are the _previous_ position and look.
Movement { Movement {
/// Absolute coordinates of the previous position. /// Absolute coordinates of the previous position.
position: Vec3<f64>, old_position: Vec3<f64>,
/// Previous velocity in m/s.
old_velocity: Vec3<f32>,
/// The previous yaw (in degrees). /// The previous yaw (in degrees).
yaw: f32, old_yaw: f32,
/// The previous pitch (in degrees). /// The previous pitch (in degrees).
pitch: f32, old_pitch: f32,
/// If the client was previously on the ground. /// If the client was previously on the ground.
on_ground: bool, old_on_ground: bool,
/// Absolute coodinates of the new position.
new_position: Vec3<f64>,
/// New velocity in m/s.
new_velocity: Vec3<f32>,
/// 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, StartSneaking,
StopSneaking, StopSneaking,

View file

@ -10,7 +10,7 @@ use crate::biome::Biome;
use crate::dimension::Dimension; use crate::dimension::Dimension;
use crate::server::{NewClientData, Server, SharedServer}; use crate::server::{NewClientData, Server, SharedServer};
use crate::text::Text; use crate::text::Text;
use crate::Ticks; use crate::{Ticks, STANDARD_TPS};
/// A trait for the configuration of a server. /// A trait for the configuration of a server.
/// ///
@ -65,9 +65,9 @@ pub trait Config: 'static + Sized + Send + Sync + UnwindSafe + RefUnwindSafe {
/// ///
/// # Default Implementation /// # Default Implementation
/// ///
/// Returns `20`, which is the same as Minecraft's official server. /// Returns [`STANDARD_TPS`].
fn tick_rate(&self) -> Ticks { fn tick_rate(&self) -> Ticks {
20 STANDARD_TPS
} }
/// Called once at startup to get the "online mode" option, which determines /// Called once at startup to get the "online mode" option, which determines

View file

@ -22,6 +22,7 @@ use crate::protocol_inner::{ByteAngle, RawBytes, VarInt};
use crate::slotmap::{Key, SlotMap}; use crate::slotmap::{Key, SlotMap};
use crate::util::aabb_from_bottom_and_size; use crate::util::aabb_from_bottom_and_size;
use crate::world::WorldId; use crate::world::WorldId;
use crate::STANDARD_TPS;
/// A container for all [`Entity`]s on a [`Server`](crate::server::Server). /// A container for all [`Entity`]s on a [`Server`](crate::server::Server).
/// ///
@ -583,7 +584,7 @@ impl<C: Config> Entity<C> {
pub(crate) fn velocity_to_packet_units(vel: Vec3<f32>) -> Vec3<i16> { pub(crate) fn velocity_to_packet_units(vel: Vec3<f32>) -> Vec3<i16> {
// The saturating cast to i16 is desirable. // The saturating cast to i16 is desirable.
(vel * 400.0).as_() (8000.0 / STANDARD_TPS as f32 * vel).as_()
} }
pub(crate) enum EntitySpawnPacket { pub(crate) enum EntitySpawnPacket {

View file

@ -168,11 +168,15 @@ pub const VERSION_NAME: &str = "1.19";
/// [identifiers](crate::ident::Ident). /// [identifiers](crate::ident::Ident).
/// ///
/// You should avoid using this namespace in your own identifiers. /// 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 /// A discrete unit of time where 1 tick is the duration of a
/// single game update. /// single game update.
/// ///
/// The duration of a game update depends on the current configuration, which /// The duration of a game update on a Valence server depends on the current
/// may or may not be the same as Minecraft's standard 20 ticks/second. /// 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; pub type Ticks = i64;
/// Minecraft's standard ticks per second (TPS).
pub const STANDARD_TPS: Ticks = 20;