From dbbcc1e80e79fc4b1e260666d5b760e8ba572cec Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Sun, 3 Mar 2019 22:02:38 +0000 Subject: [PATCH] Added basic networked communications, chat communication Former-commit-id: 06bafdf69486f4da5fbc416835e34c5bed8c2caa --- Cargo.toml | 1 + chat-cli/.gitignore | 3 + chat-cli/Cargo.toml | 12 +++ chat-cli/src/main.rs | 37 +++++++ client/src/error.rs | 17 ++++ client/src/input.rs | 14 +++ client/src/lib.rs | 110 ++++++++++++++++----- common/src/lib.rs | 1 + common/src/msg/client.rs | 5 + common/src/msg/mod.rs | 6 ++ common/src/msg/server.rs | 5 + common/src/net/postbox.rs | 2 +- common/src/net/postoffice.rs | 2 +- common/src/state.rs | 13 +++ server-cli/src/main.rs | 20 ++-- server/src/client.rs | 11 +++ server/src/error.rs | 13 +++ server/src/input.rs | 9 ++ server/src/lib.rs | 158 +++++++++++++++++++++++++++--- voxygen/src/anim/character/run.rs | 2 +- voxygen/src/scene/mod.rs | 4 +- 21 files changed, 393 insertions(+), 52 deletions(-) create mode 100644 chat-cli/.gitignore create mode 100644 chat-cli/Cargo.toml create mode 100644 chat-cli/src/main.rs create mode 100644 client/src/error.rs create mode 100644 client/src/input.rs create mode 100644 common/src/msg/client.rs create mode 100644 common/src/msg/mod.rs create mode 100644 common/src/msg/server.rs create mode 100644 server/src/client.rs create mode 100644 server/src/error.rs create mode 100644 server/src/input.rs diff --git a/Cargo.toml b/Cargo.toml index f15ce50095..533a0add9c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ members = [ "common", "client", + "chat-cli", "server", "server-cli", "voxygen", diff --git a/chat-cli/.gitignore b/chat-cli/.gitignore new file mode 100644 index 0000000000..693699042b --- /dev/null +++ b/chat-cli/.gitignore @@ -0,0 +1,3 @@ +/target +**/*.rs.bk +Cargo.lock diff --git a/chat-cli/Cargo.toml b/chat-cli/Cargo.toml new file mode 100644 index 0000000000..61161cd537 --- /dev/null +++ b/chat-cli/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "veloren-chat-cli" +version = "0.1.0" +authors = ["Joshua Barretto "] +edition = "2018" + +[dependencies] +client = { package = "veloren-client", path = "../client" } +common = { package = "veloren-common", path = "../common" } + +log = "0.4" +pretty_env_logger = "0.3" diff --git a/chat-cli/src/main.rs b/chat-cli/src/main.rs new file mode 100644 index 0000000000..548829b25c --- /dev/null +++ b/chat-cli/src/main.rs @@ -0,0 +1,37 @@ +use std::time::Duration; +use log::info; +use client::{Input, Client, Event}; +use common::clock::Clock; + +const FPS: u64 = 60; + +fn main() { + // Init logging + pretty_env_logger::init(); + + info!("Starting chat-cli..."); + + // Set up an fps clock + let mut clock = Clock::new(); + + // Create client + let mut client = Client::new(([127, 0, 0, 1], 59003)) + .expect("Failed to create client instance"); + + loop { + let events = client.tick(Input::default(), clock.get_last_delta()) + .expect("Failed to tick client"); + + for event in events { + match event { + Event::Chat(msg) => println!("[chat] {}", msg), + } + } + + // Clean up the server after a tick + client.cleanup(); + + // Wait for the next tick + clock.tick(Duration::from_millis(1000 / FPS)); + } +} diff --git a/client/src/error.rs b/client/src/error.rs new file mode 100644 index 0000000000..ac61996040 --- /dev/null +++ b/client/src/error.rs @@ -0,0 +1,17 @@ +use common::net::PostError; + +#[derive(Debug)] +pub enum Error { + Network(PostError), + ServerShutdown, + Other(String), +} + +impl From for Error { + fn from(err: PostError) -> Self { + match err { + PostError::Disconnected => Error::ServerShutdown, + err => Error::Network(err), + } + } +} diff --git a/client/src/input.rs b/client/src/input.rs new file mode 100644 index 0000000000..c30a12bbb9 --- /dev/null +++ b/client/src/input.rs @@ -0,0 +1,14 @@ +use vek::*; + +pub struct Input { + // TODO: Use this type to manage client input + pub move_dir: Vec2, +} + +impl Default for Input { + fn default() -> Self { + Input { + move_dir: Vec2::zero(), + } + } +} diff --git a/client/src/lib.rs b/client/src/lib.rs index 062f063fd7..af2f41d397 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -1,29 +1,38 @@ -// Standard -use std::time::Duration; +pub mod error; +pub mod input; -// Library -use specs::Entity as EcsEntity; +// Reexports +pub use specs::Entity as EcsEntity; +pub use crate::{ + error::Error, + input::Input, +}; + +use std::{ + time::Duration, + net::SocketAddr, +}; use vek::*; use threadpool; - -// Project -use common::{comp::phys::Vel, state::State, terrain::TerrainChunk}; +use common::{ + comp::phys::Vel, + state::State, + terrain::TerrainChunk, + net::PostBox, + msg::{ClientMsg, ServerMsg}, +}; use world::World; -#[derive(Debug)] -pub enum Error { - ServerShutdown, - Other(String), -} - -pub struct Input { - // TODO: Use this type to manage client input - pub move_dir: Vec2, +pub enum Event { + Chat(String), } pub struct Client { thread_pool: threadpool::ThreadPool, + last_ping: f64, + postbox: PostBox, + tick: u64, state: State, player: Option, @@ -35,25 +44,35 @@ pub struct Client { impl Client { /// Create a new `Client`. - pub fn new() -> Self { - Self { + #[allow(dead_code)] + pub fn new>(addr: A) -> Result { + let state = State::new(); + + let mut postbox = PostBox::to_server(addr)?; + postbox.send(ClientMsg::Chat(String::from("Hello, world!"))); + + Ok(Self { thread_pool: threadpool::Builder::new() .thread_name("veloren-worker".into()) .build(), + last_ping: state.get_time(), + postbox, + tick: 0, - state: State::new(), + state, player: None, // Testing world: World::new(), chunk: None, - } + }) } /// Get a reference to the client's worker thread pool. This pool should be used for any /// computationally expensive operations that run outside of the main thread (i.e: threads that /// block on I/O operations are exempt). + #[allow(dead_code)] pub fn thread_pool(&self) -> &threadpool::ThreadPool { &self.thread_pool } // TODO: Get rid of this @@ -70,23 +89,34 @@ impl Client { } /// Get a reference to the client's game state. + #[allow(dead_code)] pub fn state(&self) -> &State { &self.state } /// Get a mutable reference to the client's game state. + #[allow(dead_code)] pub fn state_mut(&mut self) -> &mut State { &mut self.state } /// Get the player entity + #[allow(dead_code)] pub fn player(&self) -> Option { self.player } /// Get the current tick number. + #[allow(dead_code)] pub fn get_tick(&self) -> u64 { self.tick } + /// Send a chat message to the server + #[allow(dead_code)] + pub fn send_chat(&mut self, msg: String) -> Result<(), Error> { + Ok(self.postbox.send(ClientMsg::Chat(msg))?) + } + /// Execute a single client tick, handle input and update the game state by the given duration - pub fn tick(&mut self, input: Input, dt: Duration) -> Result<(), Error> { + #[allow(dead_code)] + pub fn tick(&mut self, input: Input, dt: Duration) -> Result, Error> { // This tick function is the centre of the Veloren universe. Most client-side things are // managed from here, and as such it's important that it stays organised. Please consult // the core developers before making significant changes to this code. Here is the @@ -99,7 +129,13 @@ impl Client { // 4) Go through the terrain update queue and apply all changes to the terrain // 5) Finish the tick, passing control of the main thread back to the frontend - // (step 1) + // Build up a list of events for this frame, to be passed to the frontend + let mut frontend_events = Vec::new(); + + // Handle new messages from the server + frontend_events.append(&mut self.handle_new_messages()?); + + // Step 3 if let Some(p) = self.player { // TODO: remove this const PLAYER_VELOCITY: f32 = 100.0; @@ -113,12 +149,40 @@ impl Client { // Finish the tick, pass control back to the frontend (step 6) self.tick += 1; - Ok(()) + Ok(frontend_events) } /// Clean up the client after a tick + #[allow(dead_code)] pub fn cleanup(&mut self) { // Cleanup the local state self.state.cleanup(); } + + /// Handle new server messages + fn handle_new_messages(&mut self) -> Result, Error> { + let mut frontend_events = Vec::new(); + + // Step 1 + let new_msgs = self.postbox.new_messages(); + + if new_msgs.len() > 0 { + self.last_ping = self.state.get_time(); + + for msg in new_msgs { + match msg { + ServerMsg::Chat(msg) => frontend_events.push(Event::Chat(msg)), + ServerMsg::Shutdown => return Err(Error::ServerShutdown), + } + } + } + + Ok(frontend_events) + } +} + +impl Drop for Client { + fn drop(&mut self) { + self.postbox.send(ClientMsg::Disconnect).unwrap(); + } } diff --git a/common/src/lib.rs b/common/src/lib.rs index 2b883924a2..0b22bd70d7 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -6,6 +6,7 @@ extern crate serde_derive; pub mod clock; pub mod comp; pub mod figure; +pub mod msg; pub mod state; pub mod sys; pub mod terrain; diff --git a/common/src/msg/client.rs b/common/src/msg/client.rs new file mode 100644 index 0000000000..11d7155c2e --- /dev/null +++ b/common/src/msg/client.rs @@ -0,0 +1,5 @@ +#[derive(Debug, Serialize, Deserialize)] +pub enum ClientMsg { + Chat(String), + Disconnect, +} diff --git a/common/src/msg/mod.rs b/common/src/msg/mod.rs new file mode 100644 index 0000000000..ee18da8f4d --- /dev/null +++ b/common/src/msg/mod.rs @@ -0,0 +1,6 @@ +pub mod server; +pub mod client; + +// Reexports +pub use server::ServerMsg; +pub use client::ClientMsg; diff --git a/common/src/msg/server.rs b/common/src/msg/server.rs new file mode 100644 index 0000000000..def24a97a4 --- /dev/null +++ b/common/src/msg/server.rs @@ -0,0 +1,5 @@ +#[derive(Debug, Serialize, Deserialize)] +pub enum ServerMsg { + Chat(String), + Shutdown, +} diff --git a/common/src/net/postbox.rs b/common/src/net/postbox.rs index 87a41da4dd..4e29f47688 100644 --- a/common/src/net/postbox.rs +++ b/common/src/net/postbox.rs @@ -112,7 +112,7 @@ where /// Non-blocking receiver method returning an iterator over already received and deserialized objects /// # Errors /// If the other side disconnects PostBox won't realize that until you try to send something - pub fn recv_iter(&mut self) -> impl Iterator { + pub fn new_messages(&mut self) -> impl ExactSizeIterator { let mut events = Events::with_capacity(4096); let mut items = VecDeque::new(); diff --git a/common/src/net/postoffice.rs b/common/src/net/postoffice.rs index 479f940419..ce25fbdcaf 100644 --- a/common/src/net/postoffice.rs +++ b/common/src/net/postoffice.rs @@ -79,7 +79,7 @@ where /// Non-blocking method returning an iterator over new connections wrapped in [`PostBox`]es pub fn new_connections( &mut self, - ) -> impl Iterator> { + ) -> impl ExactSizeIterator> { let mut events = Events::with_capacity(256); let mut conns = VecDeque::new(); diff --git a/common/src/state.rs b/common/src/state.rs index df82b00dd1..1d557cc846 100644 --- a/common/src/state.rs +++ b/common/src/state.rs @@ -72,6 +72,19 @@ impl State { } } + /// Register a component with the state's ECS + pub fn with_component(mut self) -> Self + where ::Storage: Default + { + self.ecs_world.register::(); + self + } + + /// Delete an entity from the state's ECS, if it exists + pub fn delete_entity(&mut self, entity: EcsEntity) { + let _ = self.ecs_world.delete_entity(entity); + } + // TODO: Get rid of this pub fn new_test_player(&mut self) -> EcsEntity { self.ecs_world diff --git a/server-cli/src/main.rs b/server-cli/src/main.rs index a2f280f9e6..1b574a5bb3 100644 --- a/server-cli/src/main.rs +++ b/server-cli/src/main.rs @@ -1,11 +1,6 @@ -// Standard use std::time::Duration; - -// Library use log::info; - -// Project -use server::{self, Server}; +use server::{Input, Event, Server}; use common::clock::Clock; const FPS: u64 = 60; @@ -20,12 +15,21 @@ fn main() { let mut clock = Clock::new(); // Create server - let mut server = Server::new(); + let mut server = Server::new() + .expect("Failed to create server instance"); loop { - server.tick(server::Input {}, clock.get_last_delta()) + let events = server.tick(Input::default(), clock.get_last_delta()) .expect("Failed to tick server"); + for event in events { + match event { + Event::ClientConnected { ecs_entity } => println!("Client connected!"), + Event::ClientDisconnected { ecs_entity } => println!("Client disconnected!"), + Event::Chat { msg, .. } => println!("[chat] {}", msg), + } + } + // Clean up the server after a tick server.cleanup(); diff --git a/server/src/client.rs b/server/src/client.rs new file mode 100644 index 0000000000..45d42dda73 --- /dev/null +++ b/server/src/client.rs @@ -0,0 +1,11 @@ +use specs::Entity as EcsEntity; +use common::{ + msg::{ServerMsg, ClientMsg}, + net::PostBox, +}; + +pub struct Client { + pub ecs_entity: EcsEntity, + pub postbox: PostBox, + pub last_ping: f64, +} diff --git a/server/src/error.rs b/server/src/error.rs new file mode 100644 index 0000000000..c53a8910de --- /dev/null +++ b/server/src/error.rs @@ -0,0 +1,13 @@ +use common::net::PostError; + +#[derive(Debug)] +pub enum Error { + Network(PostError), + Other(String), +} + +impl From for Error { + fn from(err: PostError) -> Self { + Error::Network(err) + } +} diff --git a/server/src/input.rs b/server/src/input.rs new file mode 100644 index 0000000000..8b81afd14d --- /dev/null +++ b/server/src/input.rs @@ -0,0 +1,9 @@ +pub struct Input { + // TODO: Use this type to manage server input +} + +impl Default for Input { + fn default() -> Self { + Input {} + } +} diff --git a/server/src/lib.rs b/server/src/lib.rs index 04daffe622..fa5eb309ed 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -1,47 +1,81 @@ -// Standard -use std::time::Duration; +#![feature(drain_filter)] -// Internal -use common::state::State; +pub mod client; +pub mod error; +pub mod input; + +// Reexports +pub use crate::{ + error::Error, + input::Input, +}; + +use std::{ + time::Duration, + net::SocketAddr, +}; +use specs::Entity as EcsEntity; +use common::{ + state::State, + net::PostOffice, + msg::{ServerMsg, ClientMsg}, +}; use world::World; +use crate::client::Client; -#[derive(Debug)] -pub enum Error { - Other(String), -} +const CLIENT_TIMEOUT: f64 = 5.0; // Seconds -pub struct Input { - // TODO: Use this type to manage server input +pub enum Event { + ClientConnected { + ecs_entity: EcsEntity, + }, + ClientDisconnected { + ecs_entity: EcsEntity, + }, + Chat { + ecs_entity: EcsEntity, + msg: String, + }, } pub struct Server { state: State, world: World, - // TODO: Add "meta" state here + postoffice: PostOffice, + clients: Vec, } impl Server { /// Create a new `Server`. - pub fn new() -> Self { - Self { + #[allow(dead_code)] + pub fn new() -> Result { + Ok(Self { state: State::new(), world: World::new(), - } + + postoffice: PostOffice::new(SocketAddr::from(([0; 4], 59003)))?, + clients: Vec::new(), + }) } /// Get a reference to the server's game state. + #[allow(dead_code)] pub fn state(&self) -> &State { &self.state } /// Get a mutable reference to the server's game state. + #[allow(dead_code)] pub fn state_mut(&mut self) -> &mut State { &mut self.state } /// Get a reference to the server's world. + #[allow(dead_code)] pub fn world(&self) -> &World { &self.world } /// Get a mutable reference to the server's world. + #[allow(dead_code)] pub fn world_mut(&mut self) -> &mut World { &mut self.world } /// Execute a single server tick, handle input and update the game state by the given duration - pub fn tick(&mut self, input: Input, dt: Duration) -> Result<(), Error> { + #[allow(dead_code)] + pub fn tick(&mut self, input: Input, dt: Duration) -> Result, Error> { // This tick function is the centre of the Veloren universe. Most server-side things are // managed from here, and as such it's important that it stays organised. Please consult // the core developers before making significant changes to this code. Here is the @@ -56,16 +90,108 @@ impl Server { // 6) Send relevant state updates to all clients // 7) Finish the tick, passing control of the main thread back to the frontend + // Build up a list of events for this frame, to be passed to the frontend + let mut frontend_events = Vec::new(); + + // If networking has problems, handle them + if let Some(err) = self.postoffice.status() { + return Err(err.into()); + } + + // Handle new client connections (step 2) + frontend_events.append(&mut self.handle_new_connections()?); + + // Handle new messages from clients + frontend_events.append(&mut self.handle_new_messages()?); + // Tick the client's LocalState (step 3) self.state.tick(dt); // Finish the tick, pass control back to the frontend (step 6) - Ok(()) + Ok(frontend_events) } /// Clean up the server after a tick + #[allow(dead_code)] pub fn cleanup(&mut self) { // Cleanup the local state self.state.cleanup(); } + + /// Handle new client connections + fn handle_new_connections(&mut self) -> Result, Error> { + let mut frontend_events = Vec::new(); + + for postbox in self.postoffice.new_connections() { + // TODO: Don't use this method + let ecs_entity = self.state.new_test_player(); + + frontend_events.push(Event::ClientConnected { + ecs_entity, + }); + + self.clients.push(Client { + ecs_entity, + postbox, + last_ping: self.state.get_time(), + }); + } + + Ok(frontend_events) + } + + /// Handle new client messages + fn handle_new_messages(&mut self) -> Result, Error> { + let mut frontend_events = Vec::new(); + + let state = &mut self.state; + let mut new_chat_msgs = Vec::new(); + + self.clients.drain_filter(|client| { + let mut disconnected = false; + let new_msgs = client.postbox.new_messages(); + + // Update client ping + if new_msgs.len() > 0 { + client.last_ping = state.get_time(); + + // Process incoming messages + for msg in new_msgs { + match msg { + ClientMsg::Chat(msg) => new_chat_msgs.push((client.ecs_entity, msg)), + ClientMsg::Disconnect => disconnected = true, + } + } + } else if + state.get_time() - client.last_ping > CLIENT_TIMEOUT || + client.postbox.status().is_some() + { + disconnected = true; + } + + if disconnected { + state.delete_entity(client.ecs_entity); + frontend_events.push(Event::ClientDisconnected { + ecs_entity: client.ecs_entity, + }); + true + } else { + false + } + }); + + // Handle new chat messages + for (ecs_entity, msg) in new_chat_msgs { + for client in &mut self.clients { + let _ = client.postbox.send(ServerMsg::Chat(msg.clone())); + } + + frontend_events.push(Event::Chat { + ecs_entity, + msg, + }); + } + + Ok(frontend_events) + } } diff --git a/voxygen/src/anim/character/run.rs b/voxygen/src/anim/character/run.rs index 3ad70cb68b..5f1eff799b 100644 --- a/voxygen/src/anim/character/run.rs +++ b/voxygen/src/anim/character/run.rs @@ -35,7 +35,7 @@ impl Animation for RunAnimation { //skeleton.br_foot.offset = Vec3::new(0.0, wavecos_slow * 1.0, wave_slow * 2.0 + wave_dip * 1.0); //skeleton.br_foot.ori = Quaternion::rotation_x(0.0 + wave_slow * 10.1); - skeleton.bl_foot.offset = Vec3::new(0.0, 0.0, 80.0); + skeleton.bl_foot.offset = Vec3::new(0.0, 0.0, 0.0); skeleton.bl_foot.ori = Quaternion::rotation_x(wave_slow * 2.0); //skeleton.bl_foot.offset = Vec3::new(0.0, wavecos_slow * 1.0, wave_slow * 2.0 + wave_dip * 1.0); //skeleton.bl_foot.ori = Quaternion::rotation_x(0.5 + wave_slow * 0.1); diff --git a/voxygen/src/scene/mod.rs b/voxygen/src/scene/mod.rs index 5792d30ac7..b144ad45d4 100644 --- a/voxygen/src/scene/mod.rs +++ b/voxygen/src/scene/mod.rs @@ -84,7 +84,7 @@ impl Scene { [ Some(load_segment("dragonhead.vox").generate_mesh(Vec3::new(2.0, -12.0, 2.0))), Some(load_segment("dragon_body.vox").generate_mesh(Vec3::new(0.0, 0.0, 0.0))), - Some(load_segment("dragon_lfoot.vox").generate_mesh(Vec3::new(10.0, 10.0, -80.0))), + Some(load_segment("dragon_lfoot.vox").generate_mesh(Vec3::new(0.0, 10.0, -4.0))), Some(load_segment("dragon_rfoot.vox").generate_mesh(Vec3::new(0.0, 10.0, -4.0))), Some(load_segment("dragon_rfoot.vox").generate_mesh(Vec3::new(0.0, -10.0, -4.0))), Some(load_segment("dragon_lfoot.vox").generate_mesh(Vec3::new(0.0, 0.0, 0.0))), @@ -95,7 +95,7 @@ impl Scene { None, None, None, - None, + None, None, None, ],