diff --git a/.github/workflows/clippy.yml b/.github/workflows/clippy.yml index 37d5937..e6ae294 100644 --- a/.github/workflows/clippy.yml +++ b/.github/workflows/clippy.yml @@ -1,12 +1,12 @@ on: [push] -name: clippy +name: Clippy +# Fail on all warnings, including clippy lints. +env: + RUSTFLAGS: "-Dwarnings" jobs: clippy: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 - - run: rustup component add clippy - - uses: actions-rs/clippy-check@v1 - with: - token: ${{ secrets.GITHUB_TOKEN }} - args: --all-features -- -A clippy::pedantic \ No newline at end of file + - uses: actions/checkout@v3 + - name: Run Clippy + run: cargo clippy --all-targets --all-features diff --git a/Cargo.lock b/Cargo.lock index 468433e..60aad51 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -159,7 +159,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" [[package]] -name = "composition" +name = "composition-core" version = "0.1.0" dependencies = [ "base64", @@ -169,6 +169,7 @@ dependencies = [ "once_cell", "serde", "serde_json", + "thiserror", "tokio", "tokio-util", "toml", @@ -341,9 +342,9 @@ checksum = "6a987beff54b60ffa6d51982e1aa1146bc42f19bd26be28b0586f252fccf5317" [[package]] name = "linux-raw-sys" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b64f40e5e03e0d54f03845c8197d0291253cdbedfb1cb46b13c2c117554a9f4c" +checksum = "ece97ea872ece730aed82664c424eb4c8291e1ff2480247ccf7409044bc6479f" [[package]] name = "lock_api" @@ -472,9 +473,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.37.18" +version = "0.37.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bbfc1d1c7c40c01715f47d71444744a81669ca84e8b63e25a55e169b1f86433" +checksum = "acf8729d8542766f1b2cf77eb034d52f40d375bb8b615d0b147089946e16613d" dependencies = [ "bitflags", "errno", diff --git a/Cargo.toml b/Cargo.toml index 51b5ddf..5d3a6f3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,48 +1,29 @@ [workspace] members = ["crates/*"] +[workspace.package] +version = "0.1.0" +authors = ["Garen Tyler "] +repository = "https://github.com/garentyler/composition" +readme = "README.md" +license = "MIT" +edition = "2021" + [workspace.dependencies] anyhow = "1.0.71" apecs = "0.7.0" async-trait = "0.1.68" byteorder = "1.4.3" -composition-parsing = { path = "./crates/composition-parsing" } -composition-protocol = { path = "./crates/composition-protocol" } -composition-world = { path = "./crates/composition-world" } +composition-core.path = "./crates/composition-core" +composition-parsing.path = "./crates/composition-parsing" +composition-protocol.path = "./crates/composition-protocol" +composition-world.path = "./crates/composition-world" serde = { version = "1.0.160", features = ["serde_derive"] } serde_json = "1.0.96" thiserror = "1.0.40" tokio = { version = "1.28.0", features = ["full"] } tracing = { version = "0.1.37", features = ["log"] } -[package] -name = "composition" -version = "0.1.0" -edition = "2021" -authors = ["Garen Tyler "] -description = "An extremely fast Minecraft server" -license = "MIT" -build = "build.rs" - -[features] -default = [] -update_1_20 = ["composition-protocol/update_1_20"] - -[dependencies] -base64 = "0.21.0" -clap = { version = "4.2.7", features = ["derive"] } -composition-parsing = { workspace = true } -composition-protocol = { workspace = true } -once_cell = "1.17.1" -serde = { workspace = true } -serde_json = { workspace = true } -tokio = { workspace = true } -tokio-util = "0.7.8" -toml = "0.7.3" -tracing = { workspace = true } -tracing-subscriber = { version = "0.3.17", features = ["tracing-log"] } -tracing-appender = "0.2.2" - # Unused but possibly useful dependencies: # async-trait = "0.1.48" # backtrace = "0.3.50" diff --git a/crates/composition-core/Cargo.toml b/crates/composition-core/Cargo.toml new file mode 100644 index 0000000..3a5d15b --- /dev/null +++ b/crates/composition-core/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "composition-core" +description = "An extremely fast Minecraft server" +build = "build.rs" +authors.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +version.workspace = true + +[[bin]] +name = "composition" +path = "src/main.rs" + +[features] +default = [] +update_1_20 = ["composition-protocol/update_1_20"] + +[dependencies] +base64 = "0.21.0" +clap = { version = "4.2.7", features = ["derive"] } +composition-parsing.workspace = true +composition-protocol.workspace = true +once_cell = "1.17.1" +serde.workspace = true +serde_json.workspace = true +thiserror.workspace = true +tokio.workspace = true +tokio-util = "0.7.8" +toml = "0.7.3" +tracing.workspace = true +tracing-subscriber = { version = "0.3.17", features = ["tracing-log"] } +tracing-appender = "0.2.2" diff --git a/build.rs b/crates/composition-core/build.rs similarity index 100% rename from build.rs rename to crates/composition-core/build.rs diff --git a/src/config.rs b/crates/composition-core/src/config.rs similarity index 95% rename from src/config.rs rename to crates/composition-core/src/config.rs index 6cdfe91..41a4b2c 100644 --- a/src/config.rs +++ b/crates/composition-core/src/config.rs @@ -14,6 +14,8 @@ pub static CONFIG: OnceCell = OnceCell::new(); pub static ARGS: OnceCell = OnceCell::new(); static DEFAULT_ARGS: Lazy = Lazy::new(Args::default); +/// Helper function to read a file from a `Path` +/// and return its bytes as a `Vec`. #[tracing::instrument] fn read_file(path: &Path) -> std::io::Result> { trace!("{:?}", path); @@ -23,6 +25,7 @@ fn read_file(path: &Path) -> std::io::Result> { Ok(data) } +/// The main server configuration struct. #[derive(Debug, Deserialize, Serialize)] #[serde(rename_all = "kebab-case")] #[serde(default)] @@ -143,10 +146,13 @@ impl Config { } } +/// All of the valid command line arguments for the composition binary. +/// +/// Arguments will always override the config options specified in `composition.toml` or `Config::default()`. #[derive(Debug)] pub struct Args { - pub config_file: PathBuf, - pub server_icon: PathBuf, + config_file: PathBuf, + server_icon: PathBuf, pub log_level: Option, pub log_dir: PathBuf, } diff --git a/crates/composition-core/src/error.rs b/crates/composition-core/src/error.rs new file mode 100644 index 0000000..109bb97 --- /dev/null +++ b/crates/composition-core/src/error.rs @@ -0,0 +1,10 @@ +/// This type represents all possible errors that can occur when running the server. +#[allow(dead_code)] +#[derive(thiserror::Error, Clone, Debug, PartialEq)] +pub enum Error { + #[error("the server is not running")] + NotRunning, +} + +/// Alias for a Result with the error type `composition_core::server::Error`. +pub type Result = std::result::Result; diff --git a/crates/composition-core/src/lib.rs b/crates/composition-core/src/lib.rs new file mode 100644 index 0000000..1f31c54 --- /dev/null +++ b/crates/composition-core/src/lib.rs @@ -0,0 +1,23 @@ +/// Server configuration and cli options. +pub mod config; +/// When managing the server encounters errors. +pub(crate) mod error; +/// Network operations. +pub(crate) mod net; +/// The core server implementation. +pub(crate) mod server; + +use crate::config::Config; +use once_cell::sync::OnceCell; +use std::time::Instant; + +/// A globally accessible instant of the server's start time. +/// +/// This should be set immediately on startup. +pub static START_TIME: OnceCell = OnceCell::new(); + +/// Start the server. +#[tracing::instrument] +pub async fn start_server() -> (server::Server, tokio_util::sync::CancellationToken) { + server::Server::new(format!("0.0.0.0:{}", Config::instance().port)).await +} diff --git a/src/main.rs b/crates/composition-core/src/main.rs similarity index 80% rename from src/main.rs rename to crates/composition-core/src/main.rs index 643a2d9..22333ef 100644 --- a/src/main.rs +++ b/crates/composition-core/src/main.rs @@ -1,22 +1,22 @@ -#![deny(clippy::all)] - use tracing::{info, warn}; use tracing_subscriber::prelude::*; #[tracing::instrument] pub fn main() { - composition::START_TIME + composition_core::START_TIME .set(std::time::Instant::now()) - .expect("could not set composition::START_TIME"); + .expect("could not set composition_core::START_TIME"); // Set up logging. - let file_writer = - tracing_appender::rolling::daily(&composition::config::Args::instance().log_dir, "log"); + let file_writer = tracing_appender::rolling::daily( + &composition_core::config::Args::instance().log_dir, + "log", + ); let (file_writer, _guard) = tracing_appender::non_blocking(file_writer); tracing_subscriber::registry() .with(tracing_subscriber::filter::LevelFilter::from_level( - composition::config::Args::instance() + composition_core::config::Args::instance() .log_level .unwrap_or(if cfg!(debug_assertions) { tracing::Level::DEBUG @@ -38,7 +38,7 @@ pub fn main() { .init(); // Load the config. - let config = composition::config::Config::load(); + let config = composition_core::config::Config::load(); match config.server_threads { Some(1) => { @@ -61,10 +61,10 @@ pub fn main() { .unwrap() .block_on(async move { info!("Starting {} on port {}", config.server_version, config.port); - let (mut server, running) = composition::start_server().await; + let (mut server, running) = composition_core::start_server().await; info!( "Done! Start took {:?}", - composition::START_TIME.get().unwrap().elapsed() + composition_core::START_TIME.get().unwrap().elapsed() ); // The main server loop. diff --git a/src/net.rs b/crates/composition-core/src/net.rs similarity index 74% rename from src/net.rs rename to crates/composition-core/src/net.rs index 83b84d1..ef06006 100644 --- a/src/net.rs +++ b/crates/composition-core/src/net.rs @@ -1,22 +1,44 @@ -use crate::prelude::*; -use composition_protocol::packets::serverbound::SL00LoginStart; -use composition_protocol::{packets::GenericPacket, ClientState, ProtocolError}; -use std::sync::Arc; -use std::time::Instant; -use tokio::net::TcpStream; -use tokio::sync::RwLock; +use composition_parsing::parsable::Parsable; +use composition_protocol::{ + packets::{serverbound::SL00LoginStart, GenericPacket}, + ClientState, +}; +use std::{collections::VecDeque, sync::Arc, time::Instant}; +use tokio::io::AsyncWriteExt; +use tokio::{net::TcpStream, sync::RwLock}; +use tracing::{debug, trace, warn}; +/// Similar to `composition_protocol::ClientState`, +/// but contains more useful data for managing the client's state. #[derive(Clone, PartialEq, Debug)] -pub enum NetworkClientState { +pub(crate) enum NetworkClientState { + /// A client has established a connection with the server. + /// + /// See `composition_protocol::ClientState::Handshake` for more details. Handshake, + /// The client sent `SH00Handshake` with `next_state = ClientState::Status` + /// and is performing [server list ping](https://wiki.vg/Server_List_Ping). Status { + /// When the server receives `SS00StatusRequest`, this is set + /// to `true` and the server should send `CS00StatusResponse`. received_request: bool, + /// When the server receives `SS01PingRequest`, this is set + /// to `true` and the server should send `CS01PingResponse` + /// and set the connection state to `Disconnected`. received_ping: bool, }, + /// The client sent `SH00Handshake` with `next_state = ClientState::Login` + /// and is attempting to join the server. Login { received_start: (bool, Option), }, + /// The server sent `CL02LoginSuccess` and transitioned to `Play`. + #[allow(dead_code)] Play, + /// The client has disconnected. + /// + /// No packets should be sent or received, + /// and the `NetworkClient` should be queued for removal. Disconnected, } impl From for ClientState { @@ -42,14 +64,24 @@ impl AsRef for NetworkClientState { } } +/// A wrapper around the raw `TcpStream` that abstracts away reading/writing packets and bytes. #[derive(Debug, Clone)] -pub struct NetworkClient { +pub(crate) struct NetworkClient { + /// The `NetworkClient`'s unique id. pub id: u128, pub state: NetworkClientState, stream: Arc>, + /// Data gets appended to the back as it gets read, + /// and popped from the front as it gets parsed into packets. incoming_data: VecDeque, + /// Packets get appended to the back as they get read, + /// and popped from the front as they get handled. pub incoming_packet_queue: VecDeque, + /// Keeps track of the last time the client sent data. + /// + /// This is useful for removing clients that have timed out. pub last_received_data_time: Instant, + /// Packets get appended to the back and get popped from the front as they get sent. pub outgoing_packet_queue: VecDeque, } impl NetworkClient { @@ -99,7 +131,7 @@ impl NetworkClient { if self.read_data().await.is_err() { self.disconnect(None).await; - return Err(ProtocolError::Disconnected); + return Err(composition_protocol::Error::Disconnected); } self.incoming_data.make_contiguous(); @@ -136,7 +168,7 @@ impl NetworkClient { #[tracing::instrument] pub fn read_packet>( &mut self, - ) -> Option> { + ) -> Option> { if let Some(generic_packet) = self.incoming_packet_queue.pop_back() { if let Ok(packet) = TryInto::

::try_into(generic_packet.clone()) { Some(Ok(packet)) @@ -158,7 +190,7 @@ impl NetworkClient { for packet in packets { self.send_packet(packet) .await - .map_err(|_| ProtocolError::Disconnected)?; + .map_err(|_| composition_protocol::Error::Disconnected)?; } Ok(()) } @@ -167,7 +199,6 @@ impl NetworkClient { &self, packet: P, ) -> tokio::io::Result<()> { - use composition_parsing::Parsable; let packet: GenericPacket = packet.into(); debug!("Sending packet {:?} to client {}", packet, self.id); @@ -189,7 +220,7 @@ impl NetworkClient { #[tracing::instrument] pub async fn disconnect(&mut self, reason: Option) { use composition_protocol::packets::clientbound::{CL00Disconnect, CP17Disconnect}; - let reason = reason.unwrap_or(json!({ + let reason = reason.unwrap_or(serde_json::json!({ "text": "You have been disconnected!" })); diff --git a/src/server-icon.png b/crates/composition-core/src/server-icon.png similarity index 100% rename from src/server-icon.png rename to crates/composition-core/src/server-icon.png diff --git a/src/server/mod.rs b/crates/composition-core/src/server/mod.rs similarity index 95% rename from src/server/mod.rs rename to crates/composition-core/src/server/mod.rs index ff0d505..45de602 100644 --- a/src/server/mod.rs +++ b/crates/composition-core/src/server/mod.rs @@ -1,25 +1,19 @@ +use crate::config::Config; +use crate::error::Result; use crate::net::{NetworkClient, NetworkClientState}; -use crate::prelude::*; use composition_protocol::ClientState; use std::sync::Arc; use tokio::net::{TcpListener, ToSocketAddrs}; -use tokio::sync::RwLock; -use tokio::task::JoinHandle; +use tokio::{sync::RwLock, task::JoinHandle}; use tokio_util::sync::CancellationToken; +use tracing::{error, info, trace}; -#[derive(Clone, Debug, PartialEq)] -pub enum ServerError { - NotRunning, -} - -pub type Result = std::result::Result; - +/// The main state and logic of the program. #[derive(Debug)] pub struct Server { clients: Arc>>, net_tasks_handle: JoinHandle<()>, } - impl Server { #[tracing::instrument] pub async fn new( @@ -110,7 +104,7 @@ impl Server { let _ = client.read_packets().await; if client.send_queued_packets().await.is_err() { client - .disconnect(Some(json!({ "text": "Error writing packets." }))) + .disconnect(Some(serde_json::json!({ "text": "Error writing packets." }))) .await; } client @@ -194,7 +188,7 @@ impl Server { } else { client .disconnect(Some( - json!({ "text": "Received invalid SH00Handshake packet" }), + serde_json::json!({ "text": "Received invalid SH00Handshake packet" }), )) .await; } @@ -216,7 +210,7 @@ impl Server { let config = Config::instance(); use base64::Engine; client.queue_packet(CS00StatusResponse { - response: json!({ + response: serde_json::json!({ "version": { "name": config.game_version, "protocol": config.protocol_version @@ -286,7 +280,9 @@ impl Server { // Send disconnect messages to the clients. for client in self.clients.write().await.iter_mut() { client - .disconnect(Some(json!({ "text": "The server is shutting down." }))) + .disconnect(Some( + serde_json::json!({ "text": "The server is shutting down." }), + )) .await; } } diff --git a/crates/composition-parsing/Cargo.toml b/crates/composition-parsing/Cargo.toml index 08b9057..caab213 100644 --- a/crates/composition-parsing/Cargo.toml +++ b/crates/composition-parsing/Cargo.toml @@ -1,13 +1,14 @@ [package] name = "composition-parsing" -version = "0.1.0" -edition = "2021" -authors = ["Garen Tyler "] description = "Useful shared parsing functions" -license = "MIT" +authors.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +version.workspace = true [dependencies] -byteorder = { workspace = true } -serde_json = { workspace = true } -thiserror = { workspace = true } -tracing = { workspace = true } +byteorder.workspace = true +serde_json.workspace = true +thiserror.workspace = true +tracing.workspace = true diff --git a/crates/composition-parsing/src/error.rs b/crates/composition-parsing/src/error.rs index ec54bd7..20a9b89 100644 --- a/crates/composition-parsing/src/error.rs +++ b/crates/composition-parsing/src/error.rs @@ -1,16 +1,29 @@ +/// This type represents all possible errors that can occur when serializing or deserializing Minecraft data. #[derive(thiserror::Error, Debug)] pub enum Error { + /// This error was caused by unexpected or invalid data. #[error("invalid syntax")] Syntax, + /// This error was caused by prematurely reaching the end of the input data. #[error("unexpected end of file")] Eof, + /// This error was caused by reading a `composition_parsing::VarInt` that was longer than 5 bytes. #[error("VarInt was more than 5 bytes")] VarIntTooLong, + /// This error is a wrapper for `serde_json::Error`. #[error(transparent)] InvalidJson(#[from] serde_json::Error), + /// This error is general purpose. + /// When possible, other error variants should be used. #[error("custom error: {0}")] Message(String), } +/// Alias for a Result with the error type `composition_parsing::Error`. pub type Result = std::result::Result; + +/// Alias for a Result that helps with zero-copy parsing. +/// +/// The error type is `composition_parsing::Error`, +/// and the result type is a tuple of the remaining bytes and the parsed item. pub type ParseResult<'data, T> = Result<(&'data [u8], T)>; diff --git a/crates/composition-parsing/src/lib.rs b/crates/composition-parsing/src/lib.rs index 9577883..00eded3 100644 --- a/crates/composition-parsing/src/lib.rs +++ b/crates/composition-parsing/src/lib.rs @@ -1,12 +1,16 @@ -#![deny(clippy::all)] - +/// When serializing or deserializing data encounters errors. pub mod error; +/// The `Parsable` trait, and implementations for useful types. pub mod parsable; +/// Useful re-exports. +pub mod prelude { + pub use crate::{parsable::Parsable, take_bytes, VarInt}; +} pub use error::{Error, ParseResult, Result}; -pub use parsable::Parsable; pub use serde_json; +/// Returns a function that returns a `ParseResult<&[u8]>`, where the slice is size `num`. pub fn take_bytes(num: usize) -> impl Fn(&'_ [u8]) -> ParseResult<'_, &'_ [u8]> { move |data| { use std::cmp::Ordering; @@ -19,6 +23,10 @@ pub fn take_bytes(num: usize) -> impl Fn(&'_ [u8]) -> ParseResult<'_, &'_ [u8]> } } +/// Implementation of the protocol's VarInt type. +/// +/// Simple wrapper around an i32, but is parsed and serialized differently. +/// When the original i32 value is needed, simply `Deref` it. #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Default, Hash)] pub struct VarInt(i32); impl std::ops::Deref for VarInt { @@ -53,15 +61,6 @@ impl std::fmt::Display for VarInt { } } -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub enum ClientState { - Handshake, - Status, - Login, - Play, - Disconnected, -} - #[cfg(test)] mod tests { use super::*; diff --git a/crates/composition-parsing/src/parsable.rs b/crates/composition-parsing/src/parsable.rs index 6f5fde0..8a271d1 100644 --- a/crates/composition-parsing/src/parsable.rs +++ b/crates/composition-parsing/src/parsable.rs @@ -1,12 +1,21 @@ use crate::{take_bytes, Error, ParseResult, VarInt}; use byteorder::{BigEndian, ReadBytesExt}; +/// A structure that can be serialized and deserialized. +/// +/// Similar to serde's `Serialize` and `Deserialize` traits. pub trait Parsable { + /// Attempt to parse (deserialize) `Self` from the given byte slice. fn parse(data: &[u8]) -> ParseResult<'_, Self> where Self: Sized; + /// Serialize `self` into a vector of bytes. fn serialize(&self) -> Vec; + /// Helper to optionally parse `Self`. + /// + /// An `Option` is represented in the protocol as + /// a boolean optionally followed by `T` if the boolean was true. fn parse_optional(data: &[u8]) -> ParseResult<'_, Option> where Self: Sized, @@ -19,6 +28,10 @@ pub trait Parsable { Ok((data, None)) } } + + /// Helper to parse `num` repetitions of `Self`. + /// + /// Useful with an array of known length. fn parse_repeated(num: usize, mut data: &[u8]) -> ParseResult<'_, Vec> where Self: Sized, @@ -31,6 +44,11 @@ pub trait Parsable { } Ok((data, output)) } + + /// Helper to parse an array of `Self`, when the length is unknown. + /// + /// In the protocol, arrays are commonly prefixed with their length + /// as a `VarInt`. fn parse_vec(data: &[u8]) -> ParseResult<'_, Vec> where Self: Sized, diff --git a/crates/composition-protocol/Cargo.toml b/crates/composition-protocol/Cargo.toml index 1199b1a..e69b26b 100644 --- a/crates/composition-protocol/Cargo.toml +++ b/crates/composition-protocol/Cargo.toml @@ -1,19 +1,20 @@ [package] name = "composition-protocol" -version = "0.1.0" -edition = "2021" -authors = ["Garen Tyler "] description = "The Minecraft protocol implemented in a network-agnostic way" -license = "MIT" +authors.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +version.workspace = true [features] default = [] update_1_20 = [] [dependencies] -anyhow = { workspace = true } -byteorder = { workspace = true } -composition-parsing = { workspace = true } -serde = { workspace = true } -thiserror = { workspace = true } -tracing = { workspace = true } +anyhow.workspace = true +byteorder.workspace = true +composition-parsing.workspace = true +serde.workspace = true +thiserror.workspace = true +tracing.workspace = true diff --git a/crates/composition-protocol/src/entities/mod.rs b/crates/composition-protocol/src/entities/mod.rs index a5061e6..aa36a4d 100644 --- a/crates/composition-protocol/src/entities/mod.rs +++ b/crates/composition-protocol/src/entities/mod.rs @@ -11,7 +11,7 @@ use crate::{ blocks::BlockPosition, mctypes::{Chat, Uuid, VarInt}, }; -use composition_parsing::{Parsable, ParseResult}; +use composition_parsing::{parsable::Parsable, ParseResult}; pub type EntityId = VarInt; pub type EntityUuid = Uuid; diff --git a/crates/composition-protocol/src/error.rs b/crates/composition-protocol/src/error.rs new file mode 100644 index 0000000..6288464 --- /dev/null +++ b/crates/composition-protocol/src/error.rs @@ -0,0 +1,26 @@ +/// This type represents all possible errors that can occur in the Minecraft protocol. +#[derive(thiserror::Error, Debug)] +pub enum Error { + /// This error was caused by unexpected or invalid data. + #[error("invalid syntax")] + Syntax, + /// This error was caused by prematurely reaching the end of the input data. + #[error("unexpected end of file")] + Eof, + /// The connection did not receive data and timed out. + #[error("stream timed out")] + Timeout, + /// This error was caused by attempting to send or receive data from a disconnected client. + #[error("communicating to disconnected client")] + Disconnected, + /// This error is a wrapper for `composition_parsing::Error`. + #[error(transparent)] + ParseError(#[from] composition_parsing::Error), + /// This error is general purpose. + /// When possible, other error variants should be used. + #[error(transparent)] + Other(#[from] anyhow::Error), +} + +/// Alias for a Result with the error type `composition_protocol::Error`. +pub type Result = std::result::Result; diff --git a/crates/composition-protocol/src/lib.rs b/crates/composition-protocol/src/lib.rs index e0cc74f..e08d0ed 100644 --- a/crates/composition-protocol/src/lib.rs +++ b/crates/composition-protocol/src/lib.rs @@ -1,28 +1,45 @@ -#![deny(clippy::all)] - +/// Implementation of Minecraft's blocks. pub mod blocks; +/// Implementation of Minecraft's entities. pub mod entities; +/// When using the protocol encounters errors. +pub mod error; +/// Implementation of Minecraft's items and inventories. pub mod inventory; +/// Useful types for representing the Minecraft protocol. pub mod mctypes; +/// Network packets. +/// +/// The packet naming convention used is "DSIDName" where +/// 'D' is either 'S' for serverbound or 'C' for clientbound, +/// 'S' is the current connection state (**H**andshake, **S**tatus, **L**ogin, or **P**lay), +/// "ID" is the packet id in uppercase hexadecimal (ex. 1B, 05, 3A), +/// and "Name" is the packet's name as found on [wiki.vg](https://wiki.vg/Protocol) in PascalCase. +/// Examples include "SH00Handshake", "CP00SpawnEntity", and "SP11KeepAlive". pub mod packets; -use thiserror::Error; +pub use error::{Error, Result}; -pub use composition_parsing::ClientState; - -#[derive(Error, Debug)] -pub enum ProtocolError { - #[error("invalid data")] - InvalidData, - #[error("not enough data")] - NotEnoughData, - #[error("stream timed out")] - Timeout, - #[error("communicating to disconnected client")] +/// Enum representation of the connection's current state. +/// +/// Parsing packets requires knowing which state the connection is in. +/// [Relevant wiki.vg page](https://wiki.vg/How_to_Write_a_Server#FSM_example_of_handling_new_TCP_connections) +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum ClientState { + /// The connection is freshly established. + /// + /// The only packet in this state is `SH00Handshake`. + /// After this packet is sent, the connection immediately + /// transitions to `Status` or `Login`. + Handshake, + /// The client is performing [server list ping](https://wiki.vg/Server_List_Ping). + Status, + /// The client is attempting to join the server. + /// + /// The `Login` state includes authentication, encryption, compression, and plugins. + Login, + /// The main connection state. The client has authenticated and is playing on the server. + Play, + /// The client has disconnected, and the connection struct should be removed. No packets should be sent or received. Disconnected, - #[error(transparent)] - ParseError(#[from] composition_parsing::Error), - #[error(transparent)] - Other(#[from] anyhow::Error), } -pub type Result = std::result::Result; diff --git a/crates/composition-protocol/src/mctypes.rs b/crates/composition-protocol/src/mctypes.rs index 9bbb58d..87a111c 100644 --- a/crates/composition-protocol/src/mctypes.rs +++ b/crates/composition-protocol/src/mctypes.rs @@ -1,10 +1,14 @@ -use composition_parsing::Parsable; +use composition_parsing::parsable::Parsable; +/// Alias for a u128. pub type Uuid = u128; pub use composition_parsing::VarInt; +/// Alias for a `serde_json::Value`. pub type Json = composition_parsing::serde_json::Value; +/// Alias for a `Json`. pub type Chat = Json; +/// An implementation of the protocol's [Position](https://wiki.vg/Protocol#Position) type. #[derive(Debug, Copy, Clone, PartialEq)] pub struct Position { pub x: i32, @@ -41,6 +45,7 @@ impl Parsable for Position { } } +/// An enum of the possible difficulties in Minecraft. #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum Difficulty { Peaceful = 0, diff --git a/crates/composition-protocol/src/packets/clientbound/login.rs b/crates/composition-protocol/src/packets/clientbound/login.rs index 7b79434..e2a0c5c 100644 --- a/crates/composition-protocol/src/packets/clientbound/login.rs +++ b/crates/composition-protocol/src/packets/clientbound/login.rs @@ -1,5 +1,5 @@ use crate::mctypes::{Chat, Json, Uuid, VarInt}; -use composition_parsing::Parsable; +use composition_parsing::parsable::Parsable; #[derive(Clone, Debug, PartialEq)] pub struct CL00Disconnect { diff --git a/crates/composition-protocol/src/packets/clientbound/mod.rs b/crates/composition-protocol/src/packets/clientbound/mod.rs index 8aa039a..f061b42 100644 --- a/crates/composition-protocol/src/packets/clientbound/mod.rs +++ b/crates/composition-protocol/src/packets/clientbound/mod.rs @@ -1,5 +1,8 @@ +/// Packets for the `ClientState::Login` state. pub mod login; +/// Packets for the `ClientState::Play` state. pub mod play; +/// Packets for the `ClientState::Status` state. pub mod status; pub use login::*; diff --git a/crates/composition-protocol/src/packets/mod.rs b/crates/composition-protocol/src/packets/mod.rs index 392e83f..3221d1e 100644 --- a/crates/composition-protocol/src/packets/mod.rs +++ b/crates/composition-protocol/src/packets/mod.rs @@ -1,16 +1,16 @@ +/// Packets that are heading to the client. pub mod clientbound; +/// Packets that are heading to the server. pub mod serverbound; use crate::mctypes::VarInt; +use composition_parsing::prelude::*; -pub type PacketId = crate::mctypes::VarInt; +/// Alias for a `VarInt`. +pub type PacketId = VarInt; pub trait Packet: - std::fmt::Debug - + Clone - + TryFrom - + Into - + composition_parsing::Parsable + std::fmt::Debug + Clone + TryFrom + Into + Parsable { const ID: i32; const CLIENT_STATE: crate::ClientState; @@ -32,7 +32,7 @@ macro_rules! generic_packet { is_serverbound: bool, data: &'data [u8] ) -> composition_parsing::ParseResult<'data, Self> { - use composition_parsing::Parsable; + use composition_parsing::parsable::Parsable; tracing::trace!( "GenericPacket::parse_uncompressed: {:?} {} {:?}", client_state, @@ -60,7 +60,7 @@ macro_rules! generic_packet { is_serverbound: bool, data: &'data [u8], ) -> composition_parsing::ParseResult<'data, Self> { - use composition_parsing::Parsable; + use composition_parsing::parsable::Parsable; tracing::trace!( "GenericPacket::parse_body: {:?} {} {}", client_state, @@ -77,7 +77,7 @@ macro_rules! generic_packet { #[tracing::instrument] pub fn serialize(&self) -> (crate::packets::PacketId, Vec) { - use composition_parsing::Parsable; + use composition_parsing::parsable::Parsable; tracing::trace!("GenericPacket::serialize: {:?}", self); match self { $( @@ -145,7 +145,7 @@ macro_rules! packet { const CLIENT_STATE: crate::ClientState = $client_state; const IS_SERVERBOUND: bool = $serverbound; } - impl composition_parsing::Parsable for $packet_type { + impl composition_parsing::parsable::Parsable for $packet_type { #[tracing::instrument] fn parse<'data>(data: &'data [u8]) -> composition_parsing::ParseResult<'_, Self> { $parse_body(data) diff --git a/crates/composition-protocol/src/packets/serverbound/mod.rs b/crates/composition-protocol/src/packets/serverbound/mod.rs index bc1f6df..c393240 100644 --- a/crates/composition-protocol/src/packets/serverbound/mod.rs +++ b/crates/composition-protocol/src/packets/serverbound/mod.rs @@ -1,6 +1,10 @@ +/// Packets for the `ClientState::Handshake` state. pub mod handshake; +/// Packets for the `ClientState::Login` state. pub mod login; +/// Packets for the `ClientState::Play` state. pub mod play; +/// Packets for the `ClientState::Status` state. pub mod status; pub use handshake::*; diff --git a/crates/composition-world/Cargo.toml b/crates/composition-world/Cargo.toml index a9ce663..ea36ee5 100644 --- a/crates/composition-world/Cargo.toml +++ b/crates/composition-world/Cargo.toml @@ -1,13 +1,14 @@ [package] name = "composition-world" -version = "0.1.0" -edition = "2021" -authors = ["Garen Tyler "] description = "A Minecraft world generator implementation that allows for custom worlds" -license = "MIT" +authors.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +version.workspace = true [dependencies] -anyhow = { workspace = true } -async-trait = { workspace = true } -composition-protocol = { workspace = true } -thiserror = { workspace = true } +anyhow.workspace = true +async-trait.workspace = true +composition-protocol.workspace = true +thiserror.workspace = true diff --git a/crates/composition-world/src/chunks.rs b/crates/composition-world/src/chunks.rs index 01cce91..ddfaac8 100644 --- a/crates/composition-world/src/chunks.rs +++ b/crates/composition-world/src/chunks.rs @@ -4,6 +4,8 @@ use crate::{ }; use std::collections::HashMap; +/// `Chunk`s divide the world into smaller parts +/// and manage the blocks and entities within. #[derive(Debug, Clone)] pub struct Chunk { // blocks[x][y][z] @@ -19,9 +21,13 @@ impl Default for Chunk { } } +/// Position for a `Chunk`. +/// +/// To convert to block positions, multiply by a factor of 16. #[derive(Debug, Copy, Clone, PartialEq, Default, Eq, Hash)] pub struct ChunkPosition { pub x: i32, + pub y: i32, pub z: i32, } impl From for ChunkPosition { @@ -29,6 +35,7 @@ impl From for ChunkPosition { // Divide by 16 to get the chunk. ChunkPosition { x: value.x >> 4, + y: value.y >> 4, z: value.z >> 4, } } @@ -38,6 +45,7 @@ impl From for ChunkPosition { // Divide by 16 and convert to i32. ChunkPosition { x: (value.x / 16.0) as i32, + y: (value.y / 16.0) as i32, z: (value.z / 16.0) as i32, } } diff --git a/crates/composition-world/src/error.rs b/crates/composition-world/src/error.rs new file mode 100644 index 0000000..57bec6e --- /dev/null +++ b/crates/composition-world/src/error.rs @@ -0,0 +1,13 @@ +/// This type represents all possible errors that can occur when managing a `World`. +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("the given position was out of bounds")] + OutOfBounds, + #[error(transparent)] + Io(#[from] std::io::Error), + #[error(transparent)] + Other(#[from] anyhow::Error), +} + +/// Alias for a Result with the error type `composition_world::Error`. +pub type Result = std::result::Result; diff --git a/crates/composition-world/src/generators/mod.rs b/crates/composition-world/src/generators/mod.rs index 8b13789..62315cb 100644 --- a/crates/composition-world/src/generators/mod.rs +++ b/crates/composition-world/src/generators/mod.rs @@ -1 +1,2 @@ - +/// An implementation of Minecraft's superflat world type. +pub mod superflat; diff --git a/crates/composition-world/src/generators/superflat.rs b/crates/composition-world/src/generators/superflat.rs new file mode 100644 index 0000000..aefd13e --- /dev/null +++ b/crates/composition-world/src/generators/superflat.rs @@ -0,0 +1,4 @@ +/// An implementation of Minecraft's superflat world type. +pub struct Superflat; + +// TODO: Implement superflat world. diff --git a/crates/composition-world/src/lib.rs b/crates/composition-world/src/lib.rs index c05f581..1ea9b41 100644 --- a/crates/composition-world/src/lib.rs +++ b/crates/composition-world/src/lib.rs @@ -1,56 +1,70 @@ -#![deny(clippy::all)] - +/// Worlds are divided into chunks. pub mod chunks; +/// When managing a `World` encounters errors. +pub mod error; +/// Default implementations of `World`, such as `Superflat`. pub mod generators; +/// Useful re-exports. +pub mod prelude { + pub use crate::{chunks::Chunk, World}; +} pub use composition_protocol::{blocks, entities}; +pub use error::{Error, Result}; -use crate::chunks::ChunkPosition; -use blocks::BlockPosition; +use crate::chunks::{Chunk, ChunkPosition}; +use blocks::{Block, BlockPosition}; +use entities::{Entity, EntityId, EntityPosition}; use std::path::Path; -use thiserror::Error; +/// A `World` abstracts away world generation, updating blocks, and saving. #[async_trait::async_trait] pub trait World { /// Get the world's name. fn name() -> String; - /// Create a new world. + /// Create a new world from a seed. fn new(seed: u128) -> Self; - /// Load an existing world. - async fn load_from_dir + Send>(world_dir: P) -> Result + /// Load an existing world from a directory. + async fn load + Send>(world_dir: P) -> Result where Self: Sized; /// Save the world to a directory. - async fn save_to_dir + Send>(&self, world_dir: P) -> Result<()>; + async fn save + Send>(&self, world_dir: P) -> Result<()>; - async fn is_chunk_loaded(&self, chunk_pos: ChunkPosition) -> bool; + /// Check whether a chunk is loaded or not. + fn is_chunk_loaded(&self, chunk_pos: ChunkPosition) -> bool; + /// Load a chunk if it's unloaded, does nothing if the chunk is already loaded. async fn load_chunk(&self, chunk_pos: ChunkPosition) -> Result<()>; + /// Unload a chunk if it's loaded, does nothing if the chunk is already unloaded. async fn unload_chunk(&self, chunk_pos: ChunkPosition) -> Result<()>; - async fn get_chunk(&self, chunk_pos: ChunkPosition) -> Result; - async fn set_chunk(&self, chunk_pos: ChunkPosition, chunk: chunks::Chunk) -> Result<()>; + /// Gets a copy of the chunk at the given `ChunkPosition`. + async fn get_chunk(&self, chunk_pos: ChunkPosition) -> Result; + /// Sets the chunk at the given `ChunkPosition`. + async fn set_chunk(&self, chunk_pos: ChunkPosition, chunk: Chunk) -> Result<()>; - // Getting/setting blocks requires async because the chunk might not be loaded. - async fn get_block(&self, block_pos: BlockPosition) -> Result; - async fn set_block(&self, block_pos: BlockPosition, block: blocks::Block) -> Result<()>; + /// Get the block at the given `BlockPosition`. + /// + /// Async because the containing chunk might need to be loaded. + async fn get_block(&self, block_pos: BlockPosition) -> Result; + /// Set the block at the given `BlockPosition`. + /// + /// Async because the containing chunk might need to be loaded. + async fn set_block(&self, block_pos: BlockPosition, block: Block) -> Result<()>; - // Spawning/removing entities requires async because the chunk might not be loaded. - async fn spawn_entity( - &self, - entity_pos: entities::EntityPosition, - entity: entities::Entity, - ) -> Result; - fn get_entity(&self, entity_id: entities::EntityId) -> Result<&entities::Entity>; - fn get_entity_mut(&self, entity_id: entities::EntityId) -> Result<&mut entities::Entity>; - async fn remove_entity(&self, entity_id: entities::EntityId) -> Result<()>; + /// Spawn an entity at the given `EntityPosition`. + /// + /// Async because the containing chunk might need to be loaded. + async fn spawn_entity(&self, entity_pos: EntityPosition, entity: Entity) -> Result; + /// Get a reference to the entity with the given `EntityId`. + /// Returns Err if no entity could be found with that id. + fn get_entity(&self, entity_id: EntityId) -> Result<&Entity>; + /// Get a mutable reference to the entity with the given `EntityId`. + /// Returns Err if no entity could be found with that id. + fn get_entity_mut(&self, entity_id: EntityId) -> Result<&mut Entity>; + /// Remove the entity with the given `EntityId`. + /// + /// Async because the containing chunk might need to be loaded. + /// + /// This should not kill the entity, it should simply remove it from processing. + async fn remove_entity(&self, entity_id: EntityId) -> Result<()>; } - -#[derive(Error, Debug)] -pub enum WorldError { - #[error("the given position was out of bounds")] - OutOfBounds, - #[error(transparent)] - IoError(#[from] std::io::Error), - #[error(transparent)] - Other(#[from] anyhow::Error), -} -pub type Result = std::result::Result; diff --git a/src/lib.rs b/src/lib.rs deleted file mode 100644 index a88b70e..0000000 --- a/src/lib.rs +++ /dev/null @@ -1,34 +0,0 @@ -pub mod config; -pub mod net; -pub mod server; - -use crate::config::Config; -use once_cell::sync::OnceCell; -use std::time::Instant; - -pub static START_TIME: OnceCell = OnceCell::new(); - -/// Start the server. -#[tracing::instrument] -pub async fn start_server() -> (server::Server, tokio_util::sync::CancellationToken) { - server::Server::new(format!("0.0.0.0:{}", Config::instance().port)).await -} - -pub mod prelude { - pub use crate::config::Config; - pub use crate::START_TIME; - pub use composition_protocol::mctypes::{Chat, Json, Uuid}; - pub use serde::{Deserialize, Serialize}; - pub use serde_json::json; - pub use std::collections::VecDeque; - pub use std::io::{Read, Write}; - pub use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; - pub use tracing::{debug, error, info, trace, warn}; - #[derive(Clone, Debug, PartialEq)] - pub enum ParseError { - NotEnoughData, - InvalidData, - VarIntTooBig, - } - pub type ParseResult = Result<(T, usize), ParseError>; -} diff --git a/src/server/messages.rs b/src/server/messages.rs deleted file mode 100644 index 3a11353..0000000 --- a/src/server/messages.rs +++ /dev/null @@ -1,11 +0,0 @@ -#[derive(Debug, PartialEq, Clone)] -pub enum ServerboundMessage { - Chat(String), // The chat message. - PlayerJoin(String, String), // UUID, then username -} - -#[derive(Debug, PartialEq, Clone)] -pub enum ClientboundMessage { - Chat(String), // The chat message. - Disconnect(String), // The reason for disconnecting. -}