diff --git a/src/entity/player.rs b/src/entity/player.rs index 29852f0..cf69bf6 100644 --- a/src/entity/player.rs +++ b/src/entity/player.rs @@ -1,4 +1,4 @@ -use crate::server::NetworkClient; +use crate::server::net::NetworkClient; use crate::world::location::Location; pub struct Player { diff --git a/src/server/mod.rs b/src/server/mod.rs index ce20990..22ee6ab 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -1,14 +1,11 @@ -/// Definitions for all the packets in the Minecraft protocol. -pub mod packets; +/// Put the network client struct in its own file. +pub mod net; use crate::entity::player::Player; -use crate::{mctypes::*, CONFIG, FAVICON}; -use log::{debug, info}; -use packets::*; -use serde_json::json; +use log::info; +use net::*; use std::sync::mpsc::{self, Receiver, TryRecvError}; -use std::time::{Duration, Instant}; -use tokio::net::{TcpListener, TcpStream, ToSocketAddrs}; +use tokio::net::{TcpListener, ToSocketAddrs}; /// The struct containing all the data and running all the updates. pub struct Server { @@ -97,230 +94,3 @@ impl Server { Ok(()) } } - -/// The network client can only be in a few states, -/// this enum keeps track of that. -#[derive(PartialEq, Debug)] -pub enum NetworkClientState { - Handshake, - Status, - Login, - Play, - Disconnected, -} - -/// A wrapper to contain everything related -/// to networking for the client. -#[derive(Debug)] -pub struct NetworkClient { - pub id: u128, - pub connected: bool, - pub stream: TcpStream, - pub state: NetworkClientState, - pub uuid: Option, - pub username: Option, - pub last_keep_alive: Instant, -} -impl NetworkClient { - /// Create a new `NetworkClient` - pub fn new(stream: TcpStream, id: u128) -> NetworkClient { - NetworkClient { - id, - connected: true, - stream, - state: NetworkClientState::Handshake, - uuid: None, - username: None, - last_keep_alive: Instant::now(), - } - } - - /// Update the client. - /// - /// Updating could mean connecting new clients, reading packets, - /// writing packets, or disconnecting clients. - pub async fn update(&mut self, num_players: usize) -> tokio::io::Result<()> { - // println!("{:?}", self); - match self.state { - NetworkClientState::Handshake => { - let (_packet_length, _packet_id) = read_packet_header(&mut self.stream).await?; - let handshake = self.get_packet::().await?; - // Minecraft versions 1.8 - 1.8.9 use protocol version 47. - let compatible_versions = handshake.protocol_version == 47; - let next_state = match handshake.next_state.into() { - 1 => NetworkClientState::Status, - 2 => NetworkClientState::Login, - _ => NetworkClientState::Disconnected, - }; - self.state = next_state; - // If incompatible versions or wrong next state - if !compatible_versions { - let mut logindisconnect = LoginDisconnect::new(); - logindisconnect.reason = MCChat { - text: MCString::from("Incompatible client! Server is on 1.8.9"), - }; - self.send_packet(logindisconnect).await?; - self.state = NetworkClientState::Disconnected; - } - debug!("{:?}", handshake); - } - NetworkClientState::Status => { - let (_packet_length, _packet_id) = read_packet_header(&mut self.stream).await?; - let statusrequest = self.get_packet::().await?; - debug!("{:?}", statusrequest); - let mut statusresponse = StatusResponse::new(); - statusresponse.json_response = json!({ - "version": { - "name": "1.8.9", - "protocol": 47, - }, - "players": { - "max": CONFIG.max_players, - "online": num_players, - "sample": [ - { - "name": "shvr", - "id": "e3f58380-60bb-4714-91f2-151d525e64aa" - } - ] - }, - "description": { - "text": CONFIG.motd - }, - "favicon": format!("data:image/png;base64,{}", if FAVICON.is_ok() { radix64::STD.encode(FAVICON.as_ref().unwrap().as_slice()) } else { "".to_owned() }) - }) - .to_string() - .into(); - self.send_packet(statusresponse).await?; - let (_packet_length, _packet_id) = read_packet_header(&mut self.stream).await?; - let statusping = self.get_packet::().await?; - debug!("{:?}", statusping); - let mut statuspong = StatusPong::new(); - statuspong.payload = statusping.payload; - self.send_packet(statuspong).await?; - self.state = NetworkClientState::Disconnected; - } - NetworkClientState::Login => { - let (_packet_length, _packet_id) = read_packet_header(&mut self.stream).await?; - let loginstart = self.get_packet::().await?; - debug!("{:?}", loginstart); - // Offline mode skips encryption and compression. - // TODO: Encryption and compression - let mut loginsuccess = LoginSuccess::new(); - // We're in offline mode, so this is a temporary uuid. - // TODO: Get uuid and username from Mojang servers. - loginsuccess.uuid = "00000000-0000-3000-0000-000000000000".into(); - loginsuccess.username = loginstart.player_name; - self.uuid = Some(loginsuccess.uuid.clone().into()); - self.username = Some(loginsuccess.username.clone().into()); - self.send_packet(loginsuccess).await?; - self.state = NetworkClientState::Play; - let joingame = JoinGame::new(); - // TODO: Fill out `joingame` with actual information. - self.send_packet(joingame).await?; - let (_packet_length, _packet_id) = read_packet_header(&mut self.stream).await?; - let clientsettings = self.get_packet::().await?; - // TODO: Actually use client settings. - debug!("{:?}", clientsettings); - let helditemchange = HeldItemChange::new(); - // TODO: Retrieve selected slot from storage. - self.send_packet(helditemchange).await?; - // TODO: S->C Declare Recipes (1.16?) - // TODO: S->C Tags (1.16?) - // TODO: S->C Entity Status (optional?) - // TODO: S->C Declare Commands (1.16?) - // TODO: S->C Unlock Recipes (1.16?) - // TODO: S->C Player Position and Look - let playerpositionandlook = PlayerPositionAndLook::new(); - // TODO: Retrieve player position from storage. - self.send_packet(playerpositionandlook).await?; - // TODO: S->C Player Info (Add Player action) (1.16?) - // TODO: S->C Player Info (Update latency action) (1.16?) - // TODO: S->C Update View Position (1.16?) - // TODO: S->C Update Light (1.16?) - // TODO: S->C Chunk Data - // TODO: S->C World Border - // TODO: S->C Spawn Position - let spawnposition = SpawnPosition::new(); - self.send_packet(spawnposition).await?; - // Send initial keep alive. - self.send_chat_message("keep alive").await?; - self.keep_alive().await?; - // TODO: S->C Player Position and Look - // TODO: C->S Teleport Confirm - // TODO: C->S Player Position and Look - // TODO: C->S Client Status - // TODO: S->C inventories, entities, etc. - self.send_chat_message(format!( - "Welcome {} to the server!", - self.username.as_ref().unwrap_or(&"unknown".to_owned()) - )) - .await?; - } - NetworkClientState::Play => { - if self.last_keep_alive.elapsed() > Duration::from_millis(1000) { - self.send_chat_message("keep alive").await?; - self.keep_alive().await?; - } - } - NetworkClientState::Disconnected => { - if self.connected { - self.disconnect(None).await?; - } - } - } - Ok(()) - } - - /// Send a generic packet to the client. - pub async fn send_packet + core::fmt::Debug>( - &mut self, - packet: P, - ) -> tokio::io::Result<()> { - debug!("{:?}", packet); - Into::::into(packet).write(&mut self.stream).await - } - - /// Read a generic packet from the network. - pub async fn get_packet(&mut self) -> tokio::io::Result { - Ok(T::read(&mut self.stream).await?) - } - - /// Send the client a message in chat. - async fn send_chat_message>(&mut self, message: C) -> tokio::io::Result<()> { - let mut chatmessage = ClientboundChatMessage::new(); - chatmessage.text = message.into(); - self.send_packet(chatmessage).await?; - Ok(()) - } - - /// Disconnect the client. - /// - /// Sends `0x40 Disconnect` then waits 10 seconds before forcing the connection closed. - async fn disconnect(&mut self, reason: Option<&str>) -> tokio::io::Result<()> { - let mut disconnect = Disconnect::new(); - disconnect.reason.text = reason.unwrap_or("Disconnected").into(); - self.send_packet(disconnect).await?; - self.force_disconnect(); - Ok(()) - } - - /// Force disconnect the client by marking it for cleanup as disconnected. - fn force_disconnect(&mut self) { - self.connected = false; - self.state = NetworkClientState::Disconnected; - } - - /// Send a keep alive packet to the client. - async fn keep_alive(&mut self) -> tokio::io::Result<()> { - // Keep alive ping to client. - let clientboundkeepalive = KeepAlivePing::new(); - self.send_packet(clientboundkeepalive).await?; - // Keep alive pong to server. - let (_packet_length, _packet_id) = read_packet_header(&mut self.stream).await?; - let serverboundkeepalive = self.get_packet::().await?; - debug!("{:?}", serverboundkeepalive); - self.last_keep_alive = Instant::now(); - Ok(()) - } -} diff --git a/src/server/net/mod.rs b/src/server/net/mod.rs new file mode 100644 index 0000000..5465e4d --- /dev/null +++ b/src/server/net/mod.rs @@ -0,0 +1,239 @@ +/// Definitions for all the packets in the Minecraft protocol. +pub mod packets; + +use crate::{mctypes::*, CONFIG, FAVICON}; +use log::debug; +use packets::*; +use serde_json::json; +use std::time::{Duration, Instant}; +use tokio::net::TcpStream; + +/// The network client can only be in a few states, +/// this enum keeps track of that. +#[derive(PartialEq, Debug)] +pub enum NetworkClientState { + Handshake, + Status, + Login, + Play, + Disconnected, +} + +/// A wrapper to contain everything related +/// to networking for the client. +#[derive(Debug)] +pub struct NetworkClient { + pub id: u128, + pub connected: bool, + pub stream: TcpStream, + pub state: NetworkClientState, + pub uuid: Option, + pub username: Option, + pub last_keep_alive: Instant, +} +impl NetworkClient { + /// Create a new `NetworkClient` + pub fn new(stream: TcpStream, id: u128) -> NetworkClient { + NetworkClient { + id, + connected: true, + stream, + state: NetworkClientState::Handshake, + uuid: None, + username: None, + last_keep_alive: Instant::now(), + } + } + + /// Update the client. + /// + /// Updating could mean connecting new clients, reading packets, + /// writing packets, or disconnecting clients. + pub async fn update(&mut self, num_players: usize) -> tokio::io::Result<()> { + // println!("{:?}", self); + match self.state { + NetworkClientState::Handshake => { + let (_packet_length, _packet_id) = read_packet_header(&mut self.stream).await?; + let handshake = self.get_packet::().await?; + // Minecraft versions 1.8 - 1.8.9 use protocol version 47. + let compatible_versions = handshake.protocol_version == 47; + let next_state = match handshake.next_state.into() { + 1 => NetworkClientState::Status, + 2 => NetworkClientState::Login, + _ => NetworkClientState::Disconnected, + }; + self.state = next_state; + // If incompatible versions or wrong next state + if !compatible_versions { + let mut logindisconnect = LoginDisconnect::new(); + logindisconnect.reason = MCChat { + text: MCString::from("Incompatible client! Server is on 1.8.9"), + }; + self.send_packet(logindisconnect).await?; + self.state = NetworkClientState::Disconnected; + } + debug!("{:?}", handshake); + } + NetworkClientState::Status => { + let (_packet_length, _packet_id) = read_packet_header(&mut self.stream).await?; + let statusrequest = self.get_packet::().await?; + debug!("{:?}", statusrequest); + let mut statusresponse = StatusResponse::new(); + statusresponse.json_response = json!({ + "version": { + "name": "1.8.9", + "protocol": 47, + }, + "players": { + "max": CONFIG.max_players, + "online": num_players, + "sample": [ + { + "name": "shvr", + "id": "e3f58380-60bb-4714-91f2-151d525e64aa" + } + ] + }, + "description": { + "text": CONFIG.motd + }, + "favicon": format!("data:image/png;base64,{}", if FAVICON.is_ok() { radix64::STD.encode(FAVICON.as_ref().unwrap().as_slice()) } else { "".to_owned() }) + }) + .to_string() + .into(); + self.send_packet(statusresponse).await?; + let (_packet_length, _packet_id) = read_packet_header(&mut self.stream).await?; + let statusping = self.get_packet::().await?; + debug!("{:?}", statusping); + let mut statuspong = StatusPong::new(); + statuspong.payload = statusping.payload; + self.send_packet(statuspong).await?; + self.state = NetworkClientState::Disconnected; + } + NetworkClientState::Login => { + let (_packet_length, _packet_id) = read_packet_header(&mut self.stream).await?; + let loginstart = self.get_packet::().await?; + debug!("{:?}", loginstart); + // Offline mode skips encryption and compression. + // TODO: Encryption and compression + let mut loginsuccess = LoginSuccess::new(); + // We're in offline mode, so this is a temporary uuid. + // TODO: Get uuid and username from Mojang servers. + loginsuccess.uuid = "00000000-0000-3000-0000-000000000000".into(); + loginsuccess.username = loginstart.player_name; + self.uuid = Some(loginsuccess.uuid.clone().into()); + self.username = Some(loginsuccess.username.clone().into()); + self.send_packet(loginsuccess).await?; + self.state = NetworkClientState::Play; + let joingame = JoinGame::new(); + // TODO: Fill out `joingame` with actual information. + self.send_packet(joingame).await?; + let (_packet_length, _packet_id) = read_packet_header(&mut self.stream).await?; + let clientsettings = self.get_packet::().await?; + // TODO: Actually use client settings. + debug!("{:?}", clientsettings); + let helditemchange = HeldItemChange::new(); + // TODO: Retrieve selected slot from storage. + self.send_packet(helditemchange).await?; + // TODO: S->C Declare Recipes (1.16?) + // TODO: S->C Tags (1.16?) + // TODO: S->C Entity Status (optional?) + // TODO: S->C Declare Commands (1.16?) + // TODO: S->C Unlock Recipes (1.16?) + // TODO: S->C Player Position and Look + let playerpositionandlook = PlayerPositionAndLook::new(); + // TODO: Retrieve player position from storage. + self.send_packet(playerpositionandlook).await?; + // TODO: S->C Player Info (Add Player action) (1.16?) + // TODO: S->C Player Info (Update latency action) (1.16?) + // TODO: S->C Update View Position (1.16?) + // TODO: S->C Update Light (1.16?) + // TODO: S->C Chunk Data + // TODO: S->C World Border + // TODO: S->C Spawn Position + let spawnposition = SpawnPosition::new(); + self.send_packet(spawnposition).await?; + // Send initial keep alive. + self.send_chat_message("keep alive").await?; + self.keep_alive().await?; + // TODO: S->C Player Position and Look + // TODO: C->S Teleport Confirm + // TODO: C->S Player Position and Look + // TODO: C->S Client Status + // TODO: S->C inventories, entities, etc. + self.send_chat_message(format!( + "Welcome {} to the server!", + self.username.as_ref().unwrap_or(&"unknown".to_owned()) + )) + .await?; + } + NetworkClientState::Play => { + if self.last_keep_alive.elapsed() > Duration::from_millis(1000) { + self.send_chat_message("keep alive").await?; + self.keep_alive().await?; + } + } + NetworkClientState::Disconnected => { + if self.connected { + self.disconnect(None).await?; + } + } + } + Ok(()) + } + + /// Send a generic packet to the client. + pub async fn send_packet + core::fmt::Debug>( + &mut self, + packet: P, + ) -> tokio::io::Result<()> { + debug!("{:?}", packet); + Into::::into(packet).write(&mut self.stream).await + } + + /// Read a generic packet from the network. + pub async fn get_packet(&mut self) -> tokio::io::Result { + Ok(T::read(&mut self.stream).await?) + } + + /// Send the client a message in chat. + pub async fn send_chat_message>( + &mut self, + message: C, + ) -> tokio::io::Result<()> { + let mut chatmessage = ClientboundChatMessage::new(); + chatmessage.text = message.into(); + self.send_packet(chatmessage).await?; + Ok(()) + } + + /// Disconnect the client. + /// + /// Sends `0x40 Disconnect` then waits 10 seconds before forcing the connection closed. + pub async fn disconnect(&mut self, reason: Option<&str>) -> tokio::io::Result<()> { + let mut disconnect = Disconnect::new(); + disconnect.reason.text = reason.unwrap_or("Disconnected").into(); + self.send_packet(disconnect).await?; + self.force_disconnect(); + Ok(()) + } + + /// Force disconnect the client by marking it for cleanup as disconnected. + pub fn force_disconnect(&mut self) { + self.connected = false; + self.state = NetworkClientState::Disconnected; + } + + /// Send a keep alive packet to the client. + async fn keep_alive(&mut self) -> tokio::io::Result<()> { + // Keep alive ping to client. + let clientboundkeepalive = KeepAlivePing::new(); + self.send_packet(clientboundkeepalive).await?; + // Keep alive pong to server. + let (_packet_length, _packet_id) = read_packet_header(&mut self.stream).await?; + let serverboundkeepalive = self.get_packet::().await?; + debug!("{:?}", serverboundkeepalive); + self.last_keep_alive = Instant::now(); + Ok(()) + } +} diff --git a/src/server/packets/clientbound.rs b/src/server/net/packets/clientbound.rs similarity index 100% rename from src/server/packets/clientbound.rs rename to src/server/net/packets/clientbound.rs diff --git a/src/server/packets/mod.rs b/src/server/net/packets/mod.rs similarity index 100% rename from src/server/packets/mod.rs rename to src/server/net/packets/mod.rs diff --git a/src/server/packets/serverbound.rs b/src/server/net/packets/serverbound.rs similarity index 100% rename from src/server/packets/serverbound.rs rename to src/server/net/packets/serverbound.rs