Write documentation and convert to virtual workspace

This commit is contained in:
Garen Tyler 2023-05-04 15:11:31 -06:00
parent e5cb669c73
commit 5dc9d3bb8a
Signed by: garentyler
GPG Key ID: D7A048C454CB7054
33 changed files with 399 additions and 235 deletions

View File

@ -1,12 +1,12 @@
on: [push] on: [push]
name: clippy name: Clippy
# Fail on all warnings, including clippy lints.
env:
RUSTFLAGS: "-Dwarnings"
jobs: jobs:
clippy: clippy:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v1 - uses: actions/checkout@v3
- run: rustup component add clippy - name: Run Clippy
- uses: actions-rs/clippy-check@v1 run: cargo clippy --all-targets --all-features
with:
token: ${{ secrets.GITHUB_TOKEN }}
args: --all-features -- -A clippy::pedantic

11
Cargo.lock generated
View File

@ -159,7 +159,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
[[package]] [[package]]
name = "composition" name = "composition-core"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"base64", "base64",
@ -169,6 +169,7 @@ dependencies = [
"once_cell", "once_cell",
"serde", "serde",
"serde_json", "serde_json",
"thiserror",
"tokio", "tokio",
"tokio-util", "tokio-util",
"toml", "toml",
@ -341,9 +342,9 @@ checksum = "6a987beff54b60ffa6d51982e1aa1146bc42f19bd26be28b0586f252fccf5317"
[[package]] [[package]]
name = "linux-raw-sys" name = "linux-raw-sys"
version = "0.3.6" version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b64f40e5e03e0d54f03845c8197d0291253cdbedfb1cb46b13c2c117554a9f4c" checksum = "ece97ea872ece730aed82664c424eb4c8291e1ff2480247ccf7409044bc6479f"
[[package]] [[package]]
name = "lock_api" name = "lock_api"
@ -472,9 +473,9 @@ dependencies = [
[[package]] [[package]]
name = "rustix" name = "rustix"
version = "0.37.18" version = "0.37.19"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8bbfc1d1c7c40c01715f47d71444744a81669ca84e8b63e25a55e169b1f86433" checksum = "acf8729d8542766f1b2cf77eb034d52f40d375bb8b615d0b147089946e16613d"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"errno", "errno",

View File

@ -1,48 +1,29 @@
[workspace] [workspace]
members = ["crates/*"] members = ["crates/*"]
[workspace.package]
version = "0.1.0"
authors = ["Garen Tyler <garentyler@garen.dev>"]
repository = "https://github.com/garentyler/composition"
readme = "README.md"
license = "MIT"
edition = "2021"
[workspace.dependencies] [workspace.dependencies]
anyhow = "1.0.71" anyhow = "1.0.71"
apecs = "0.7.0" apecs = "0.7.0"
async-trait = "0.1.68" async-trait = "0.1.68"
byteorder = "1.4.3" byteorder = "1.4.3"
composition-parsing = { path = "./crates/composition-parsing" } composition-core.path = "./crates/composition-core"
composition-protocol = { path = "./crates/composition-protocol" } composition-parsing.path = "./crates/composition-parsing"
composition-world = { path = "./crates/composition-world" } composition-protocol.path = "./crates/composition-protocol"
composition-world.path = "./crates/composition-world"
serde = { version = "1.0.160", features = ["serde_derive"] } serde = { version = "1.0.160", features = ["serde_derive"] }
serde_json = "1.0.96" serde_json = "1.0.96"
thiserror = "1.0.40" thiserror = "1.0.40"
tokio = { version = "1.28.0", features = ["full"] } tokio = { version = "1.28.0", features = ["full"] }
tracing = { version = "0.1.37", features = ["log"] } tracing = { version = "0.1.37", features = ["log"] }
[package]
name = "composition"
version = "0.1.0"
edition = "2021"
authors = ["Garen Tyler <garentyler@garen.dev>"]
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: # Unused but possibly useful dependencies:
# async-trait = "0.1.48" # async-trait = "0.1.48"
# backtrace = "0.3.50" # backtrace = "0.3.50"

View File

@ -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"

View File

@ -14,6 +14,8 @@ pub static CONFIG: OnceCell<Config> = OnceCell::new();
pub static ARGS: OnceCell<Args> = OnceCell::new(); pub static ARGS: OnceCell<Args> = OnceCell::new();
static DEFAULT_ARGS: Lazy<Args> = Lazy::new(Args::default); static DEFAULT_ARGS: Lazy<Args> = Lazy::new(Args::default);
/// Helper function to read a file from a `Path`
/// and return its bytes as a `Vec<u8>`.
#[tracing::instrument] #[tracing::instrument]
fn read_file(path: &Path) -> std::io::Result<Vec<u8>> { fn read_file(path: &Path) -> std::io::Result<Vec<u8>> {
trace!("{:?}", path); trace!("{:?}", path);
@ -23,6 +25,7 @@ fn read_file(path: &Path) -> std::io::Result<Vec<u8>> {
Ok(data) Ok(data)
} }
/// The main server configuration struct.
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")] #[serde(rename_all = "kebab-case")]
#[serde(default)] #[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)] #[derive(Debug)]
pub struct Args { pub struct Args {
pub config_file: PathBuf, config_file: PathBuf,
pub server_icon: PathBuf, server_icon: PathBuf,
pub log_level: Option<tracing::Level>, pub log_level: Option<tracing::Level>,
pub log_dir: PathBuf, pub log_dir: PathBuf,
} }

View File

@ -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<T> = std::result::Result<T, Error>;

View File

@ -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<Instant> = 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
}

View File

@ -1,22 +1,22 @@
#![deny(clippy::all)]
use tracing::{info, warn}; use tracing::{info, warn};
use tracing_subscriber::prelude::*; use tracing_subscriber::prelude::*;
#[tracing::instrument] #[tracing::instrument]
pub fn main() { pub fn main() {
composition::START_TIME composition_core::START_TIME
.set(std::time::Instant::now()) .set(std::time::Instant::now())
.expect("could not set composition::START_TIME"); .expect("could not set composition_core::START_TIME");
// Set up logging. // Set up logging.
let file_writer = let file_writer = tracing_appender::rolling::daily(
tracing_appender::rolling::daily(&composition::config::Args::instance().log_dir, "log"); &composition_core::config::Args::instance().log_dir,
"log",
);
let (file_writer, _guard) = tracing_appender::non_blocking(file_writer); let (file_writer, _guard) = tracing_appender::non_blocking(file_writer);
tracing_subscriber::registry() tracing_subscriber::registry()
.with(tracing_subscriber::filter::LevelFilter::from_level( .with(tracing_subscriber::filter::LevelFilter::from_level(
composition::config::Args::instance() composition_core::config::Args::instance()
.log_level .log_level
.unwrap_or(if cfg!(debug_assertions) { .unwrap_or(if cfg!(debug_assertions) {
tracing::Level::DEBUG tracing::Level::DEBUG
@ -38,7 +38,7 @@ pub fn main() {
.init(); .init();
// Load the config. // Load the config.
let config = composition::config::Config::load(); let config = composition_core::config::Config::load();
match config.server_threads { match config.server_threads {
Some(1) => { Some(1) => {
@ -61,10 +61,10 @@ pub fn main() {
.unwrap() .unwrap()
.block_on(async move { .block_on(async move {
info!("Starting {} on port {}", config.server_version, config.port); 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!( info!(
"Done! Start took {:?}", "Done! Start took {:?}",
composition::START_TIME.get().unwrap().elapsed() composition_core::START_TIME.get().unwrap().elapsed()
); );
// The main server loop. // The main server loop.

View File

@ -1,22 +1,44 @@
use crate::prelude::*; use composition_parsing::parsable::Parsable;
use composition_protocol::packets::serverbound::SL00LoginStart; use composition_protocol::{
use composition_protocol::{packets::GenericPacket, ClientState, ProtocolError}; packets::{serverbound::SL00LoginStart, GenericPacket},
use std::sync::Arc; ClientState,
use std::time::Instant; };
use tokio::net::TcpStream; use std::{collections::VecDeque, sync::Arc, time::Instant};
use tokio::sync::RwLock; 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)] #[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, Handshake,
/// The client sent `SH00Handshake` with `next_state = ClientState::Status`
/// and is performing [server list ping](https://wiki.vg/Server_List_Ping).
Status { Status {
/// When the server receives `SS00StatusRequest`, this is set
/// to `true` and the server should send `CS00StatusResponse`.
received_request: bool, 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, received_ping: bool,
}, },
/// The client sent `SH00Handshake` with `next_state = ClientState::Login`
/// and is attempting to join the server.
Login { Login {
received_start: (bool, Option<SL00LoginStart>), received_start: (bool, Option<SL00LoginStart>),
}, },
/// The server sent `CL02LoginSuccess` and transitioned to `Play`.
#[allow(dead_code)]
Play, Play,
/// The client has disconnected.
///
/// No packets should be sent or received,
/// and the `NetworkClient` should be queued for removal.
Disconnected, Disconnected,
} }
impl From<NetworkClientState> for ClientState { impl From<NetworkClientState> for ClientState {
@ -42,14 +64,24 @@ impl AsRef<ClientState> for NetworkClientState {
} }
} }
/// A wrapper around the raw `TcpStream` that abstracts away reading/writing packets and bytes.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct NetworkClient { pub(crate) struct NetworkClient {
/// The `NetworkClient`'s unique id.
pub id: u128, pub id: u128,
pub state: NetworkClientState, pub state: NetworkClientState,
stream: Arc<RwLock<TcpStream>>, stream: Arc<RwLock<TcpStream>>,
/// Data gets appended to the back as it gets read,
/// and popped from the front as it gets parsed into packets.
incoming_data: VecDeque<u8>, incoming_data: VecDeque<u8>,
/// Packets get appended to the back as they get read,
/// and popped from the front as they get handled.
pub incoming_packet_queue: VecDeque<GenericPacket>, pub incoming_packet_queue: VecDeque<GenericPacket>,
/// 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, 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<GenericPacket>, pub outgoing_packet_queue: VecDeque<GenericPacket>,
} }
impl NetworkClient { impl NetworkClient {
@ -99,7 +131,7 @@ impl NetworkClient {
if self.read_data().await.is_err() { if self.read_data().await.is_err() {
self.disconnect(None).await; self.disconnect(None).await;
return Err(ProtocolError::Disconnected); return Err(composition_protocol::Error::Disconnected);
} }
self.incoming_data.make_contiguous(); self.incoming_data.make_contiguous();
@ -136,7 +168,7 @@ impl NetworkClient {
#[tracing::instrument] #[tracing::instrument]
pub fn read_packet<P: std::fmt::Debug + TryFrom<GenericPacket>>( pub fn read_packet<P: std::fmt::Debug + TryFrom<GenericPacket>>(
&mut self, &mut self,
) -> Option<Result<P, GenericPacket>> { ) -> Option<std::result::Result<P, GenericPacket>> {
if let Some(generic_packet) = self.incoming_packet_queue.pop_back() { if let Some(generic_packet) = self.incoming_packet_queue.pop_back() {
if let Ok(packet) = TryInto::<P>::try_into(generic_packet.clone()) { if let Ok(packet) = TryInto::<P>::try_into(generic_packet.clone()) {
Some(Ok(packet)) Some(Ok(packet))
@ -158,7 +190,7 @@ impl NetworkClient {
for packet in packets { for packet in packets {
self.send_packet(packet) self.send_packet(packet)
.await .await
.map_err(|_| ProtocolError::Disconnected)?; .map_err(|_| composition_protocol::Error::Disconnected)?;
} }
Ok(()) Ok(())
} }
@ -167,7 +199,6 @@ impl NetworkClient {
&self, &self,
packet: P, packet: P,
) -> tokio::io::Result<()> { ) -> tokio::io::Result<()> {
use composition_parsing::Parsable;
let packet: GenericPacket = packet.into(); let packet: GenericPacket = packet.into();
debug!("Sending packet {:?} to client {}", packet, self.id); debug!("Sending packet {:?} to client {}", packet, self.id);
@ -189,7 +220,7 @@ impl NetworkClient {
#[tracing::instrument] #[tracing::instrument]
pub async fn disconnect(&mut self, reason: Option<composition_protocol::mctypes::Chat>) { pub async fn disconnect(&mut self, reason: Option<composition_protocol::mctypes::Chat>) {
use composition_protocol::packets::clientbound::{CL00Disconnect, CP17Disconnect}; 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!" "text": "You have been disconnected!"
})); }));

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -1,25 +1,19 @@
use crate::config::Config;
use crate::error::Result;
use crate::net::{NetworkClient, NetworkClientState}; use crate::net::{NetworkClient, NetworkClientState};
use crate::prelude::*;
use composition_protocol::ClientState; use composition_protocol::ClientState;
use std::sync::Arc; use std::sync::Arc;
use tokio::net::{TcpListener, ToSocketAddrs}; use tokio::net::{TcpListener, ToSocketAddrs};
use tokio::sync::RwLock; use tokio::{sync::RwLock, task::JoinHandle};
use tokio::task::JoinHandle;
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
use tracing::{error, info, trace};
#[derive(Clone, Debug, PartialEq)] /// The main state and logic of the program.
pub enum ServerError {
NotRunning,
}
pub type Result<T> = std::result::Result<T, ServerError>;
#[derive(Debug)] #[derive(Debug)]
pub struct Server { pub struct Server {
clients: Arc<RwLock<Vec<NetworkClient>>>, clients: Arc<RwLock<Vec<NetworkClient>>>,
net_tasks_handle: JoinHandle<()>, net_tasks_handle: JoinHandle<()>,
} }
impl Server { impl Server {
#[tracing::instrument] #[tracing::instrument]
pub async fn new<A: 'static + ToSocketAddrs + Send + std::fmt::Debug>( pub async fn new<A: 'static + ToSocketAddrs + Send + std::fmt::Debug>(
@ -110,7 +104,7 @@ impl Server {
let _ = client.read_packets().await; let _ = client.read_packets().await;
if client.send_queued_packets().await.is_err() { if client.send_queued_packets().await.is_err() {
client client
.disconnect(Some(json!({ "text": "Error writing packets." }))) .disconnect(Some(serde_json::json!({ "text": "Error writing packets." })))
.await; .await;
} }
client client
@ -194,7 +188,7 @@ impl Server {
} else { } else {
client client
.disconnect(Some( .disconnect(Some(
json!({ "text": "Received invalid SH00Handshake packet" }), serde_json::json!({ "text": "Received invalid SH00Handshake packet" }),
)) ))
.await; .await;
} }
@ -216,7 +210,7 @@ impl Server {
let config = Config::instance(); let config = Config::instance();
use base64::Engine; use base64::Engine;
client.queue_packet(CS00StatusResponse { client.queue_packet(CS00StatusResponse {
response: json!({ response: serde_json::json!({
"version": { "version": {
"name": config.game_version, "name": config.game_version,
"protocol": config.protocol_version "protocol": config.protocol_version
@ -286,7 +280,9 @@ impl Server {
// Send disconnect messages to the clients. // Send disconnect messages to the clients.
for client in self.clients.write().await.iter_mut() { for client in self.clients.write().await.iter_mut() {
client client
.disconnect(Some(json!({ "text": "The server is shutting down." }))) .disconnect(Some(
serde_json::json!({ "text": "The server is shutting down." }),
))
.await; .await;
} }
} }

View File

@ -1,13 +1,14 @@
[package] [package]
name = "composition-parsing" name = "composition-parsing"
version = "0.1.0"
edition = "2021"
authors = ["Garen Tyler <garentyler@garen.dev>"]
description = "Useful shared parsing functions" description = "Useful shared parsing functions"
license = "MIT" authors.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
version.workspace = true
[dependencies] [dependencies]
byteorder = { workspace = true } byteorder.workspace = true
serde_json = { workspace = true } serde_json.workspace = true
thiserror = { workspace = true } thiserror.workspace = true
tracing = { workspace = true } tracing.workspace = true

View File

@ -1,16 +1,29 @@
/// This type represents all possible errors that can occur when serializing or deserializing Minecraft data.
#[derive(thiserror::Error, Debug)] #[derive(thiserror::Error, Debug)]
pub enum Error { pub enum Error {
/// This error was caused by unexpected or invalid data.
#[error("invalid syntax")] #[error("invalid syntax")]
Syntax, Syntax,
/// This error was caused by prematurely reaching the end of the input data.
#[error("unexpected end of file")] #[error("unexpected end of file")]
Eof, Eof,
/// This error was caused by reading a `composition_parsing::VarInt` that was longer than 5 bytes.
#[error("VarInt was more than 5 bytes")] #[error("VarInt was more than 5 bytes")]
VarIntTooLong, VarIntTooLong,
/// This error is a wrapper for `serde_json::Error`.
#[error(transparent)] #[error(transparent)]
InvalidJson(#[from] serde_json::Error), InvalidJson(#[from] serde_json::Error),
/// This error is general purpose.
/// When possible, other error variants should be used.
#[error("custom error: {0}")] #[error("custom error: {0}")]
Message(String), Message(String),
} }
/// Alias for a Result with the error type `composition_parsing::Error`.
pub type Result<T> = std::result::Result<T, Error>; pub type Result<T> = std::result::Result<T, Error>;
/// 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)>; pub type ParseResult<'data, T> = Result<(&'data [u8], T)>;

View File

@ -1,12 +1,16 @@
#![deny(clippy::all)] /// When serializing or deserializing data encounters errors.
pub mod error; pub mod error;
/// The `Parsable` trait, and implementations for useful types.
pub mod parsable; 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 error::{Error, ParseResult, Result};
pub use parsable::Parsable;
pub use serde_json; 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]> { pub fn take_bytes(num: usize) -> impl Fn(&'_ [u8]) -> ParseResult<'_, &'_ [u8]> {
move |data| { move |data| {
use std::cmp::Ordering; 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)] #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Default, Hash)]
pub struct VarInt(i32); pub struct VarInt(i32);
impl std::ops::Deref for VarInt { 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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View File

@ -1,12 +1,21 @@
use crate::{take_bytes, Error, ParseResult, VarInt}; use crate::{take_bytes, Error, ParseResult, VarInt};
use byteorder::{BigEndian, ReadBytesExt}; use byteorder::{BigEndian, ReadBytesExt};
/// A structure that can be serialized and deserialized.
///
/// Similar to serde's `Serialize` and `Deserialize` traits.
pub trait Parsable { pub trait Parsable {
/// Attempt to parse (deserialize) `Self` from the given byte slice.
fn parse(data: &[u8]) -> ParseResult<'_, Self> fn parse(data: &[u8]) -> ParseResult<'_, Self>
where where
Self: Sized; Self: Sized;
/// Serialize `self` into a vector of bytes.
fn serialize(&self) -> Vec<u8>; fn serialize(&self) -> Vec<u8>;
/// Helper to optionally parse `Self`.
///
/// An `Option<T>` is represented in the protocol as
/// a boolean optionally followed by `T` if the boolean was true.
fn parse_optional(data: &[u8]) -> ParseResult<'_, Option<Self>> fn parse_optional(data: &[u8]) -> ParseResult<'_, Option<Self>>
where where
Self: Sized, Self: Sized,
@ -19,6 +28,10 @@ pub trait Parsable {
Ok((data, None)) 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<Self>> fn parse_repeated(num: usize, mut data: &[u8]) -> ParseResult<'_, Vec<Self>>
where where
Self: Sized, Self: Sized,
@ -31,6 +44,11 @@ pub trait Parsable {
} }
Ok((data, output)) 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<Self>> fn parse_vec(data: &[u8]) -> ParseResult<'_, Vec<Self>>
where where
Self: Sized, Self: Sized,

View File

@ -1,19 +1,20 @@
[package] [package]
name = "composition-protocol" name = "composition-protocol"
version = "0.1.0"
edition = "2021"
authors = ["Garen Tyler <garentyler@garen.dev>"]
description = "The Minecraft protocol implemented in a network-agnostic way" 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] [features]
default = [] default = []
update_1_20 = [] update_1_20 = []
[dependencies] [dependencies]
anyhow = { workspace = true } anyhow.workspace = true
byteorder = { workspace = true } byteorder.workspace = true
composition-parsing = { workspace = true } composition-parsing.workspace = true
serde = { workspace = true } serde.workspace = true
thiserror = { workspace = true } thiserror.workspace = true
tracing = { workspace = true } tracing.workspace = true

View File

@ -11,7 +11,7 @@ use crate::{
blocks::BlockPosition, blocks::BlockPosition,
mctypes::{Chat, Uuid, VarInt}, mctypes::{Chat, Uuid, VarInt},
}; };
use composition_parsing::{Parsable, ParseResult}; use composition_parsing::{parsable::Parsable, ParseResult};
pub type EntityId = VarInt; pub type EntityId = VarInt;
pub type EntityUuid = Uuid; pub type EntityUuid = Uuid;

View File

@ -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<T> = std::result::Result<T, Error>;

View File

@ -1,28 +1,45 @@
#![deny(clippy::all)] /// Implementation of Minecraft's blocks.
pub mod blocks; pub mod blocks;
/// Implementation of Minecraft's entities.
pub mod entities; pub mod entities;
/// When using the protocol encounters errors.
pub mod error;
/// Implementation of Minecraft's items and inventories.
pub mod inventory; pub mod inventory;
/// Useful types for representing the Minecraft protocol.
pub mod mctypes; 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; pub mod packets;
use thiserror::Error; pub use error::{Error, Result};
pub use composition_parsing::ClientState; /// Enum representation of the connection's current state.
///
#[derive(Error, Debug)] /// Parsing packets requires knowing which state the connection is in.
pub enum ProtocolError { /// [Relevant wiki.vg page](https://wiki.vg/How_to_Write_a_Server#FSM_example_of_handling_new_TCP_connections)
#[error("invalid data")] #[derive(Debug, Copy, Clone, PartialEq, Eq)]
InvalidData, pub enum ClientState {
#[error("not enough data")] /// The connection is freshly established.
NotEnoughData, ///
#[error("stream timed out")] /// The only packet in this state is `SH00Handshake`.
Timeout, /// After this packet is sent, the connection immediately
#[error("communicating to disconnected client")] /// 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, Disconnected,
#[error(transparent)]
ParseError(#[from] composition_parsing::Error),
#[error(transparent)]
Other(#[from] anyhow::Error),
} }
pub type Result<T> = std::result::Result<T, ProtocolError>;

View File

@ -1,10 +1,14 @@
use composition_parsing::Parsable; use composition_parsing::parsable::Parsable;
/// Alias for a u128.
pub type Uuid = u128; pub type Uuid = u128;
pub use composition_parsing::VarInt; pub use composition_parsing::VarInt;
/// Alias for a `serde_json::Value`.
pub type Json = composition_parsing::serde_json::Value; pub type Json = composition_parsing::serde_json::Value;
/// Alias for a `Json`.
pub type Chat = Json; pub type Chat = Json;
/// An implementation of the protocol's [Position](https://wiki.vg/Protocol#Position) type.
#[derive(Debug, Copy, Clone, PartialEq)] #[derive(Debug, Copy, Clone, PartialEq)]
pub struct Position { pub struct Position {
pub x: i32, 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)] #[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum Difficulty { pub enum Difficulty {
Peaceful = 0, Peaceful = 0,

View File

@ -1,5 +1,5 @@
use crate::mctypes::{Chat, Json, Uuid, VarInt}; use crate::mctypes::{Chat, Json, Uuid, VarInt};
use composition_parsing::Parsable; use composition_parsing::parsable::Parsable;
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub struct CL00Disconnect { pub struct CL00Disconnect {

View File

@ -1,5 +1,8 @@
/// Packets for the `ClientState::Login` state.
pub mod login; pub mod login;
/// Packets for the `ClientState::Play` state.
pub mod play; pub mod play;
/// Packets for the `ClientState::Status` state.
pub mod status; pub mod status;
pub use login::*; pub use login::*;

View File

@ -1,16 +1,16 @@
/// Packets that are heading to the client.
pub mod clientbound; pub mod clientbound;
/// Packets that are heading to the server.
pub mod serverbound; pub mod serverbound;
use crate::mctypes::VarInt; 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: pub trait Packet:
std::fmt::Debug std::fmt::Debug + Clone + TryFrom<GenericPacket> + Into<GenericPacket> + Parsable
+ Clone
+ TryFrom<GenericPacket>
+ Into<GenericPacket>
+ composition_parsing::Parsable
{ {
const ID: i32; const ID: i32;
const CLIENT_STATE: crate::ClientState; const CLIENT_STATE: crate::ClientState;
@ -32,7 +32,7 @@ macro_rules! generic_packet {
is_serverbound: bool, is_serverbound: bool,
data: &'data [u8] data: &'data [u8]
) -> composition_parsing::ParseResult<'data, Self> { ) -> composition_parsing::ParseResult<'data, Self> {
use composition_parsing::Parsable; use composition_parsing::parsable::Parsable;
tracing::trace!( tracing::trace!(
"GenericPacket::parse_uncompressed: {:?} {} {:?}", "GenericPacket::parse_uncompressed: {:?} {} {:?}",
client_state, client_state,
@ -60,7 +60,7 @@ macro_rules! generic_packet {
is_serverbound: bool, is_serverbound: bool,
data: &'data [u8], data: &'data [u8],
) -> composition_parsing::ParseResult<'data, Self> { ) -> composition_parsing::ParseResult<'data, Self> {
use composition_parsing::Parsable; use composition_parsing::parsable::Parsable;
tracing::trace!( tracing::trace!(
"GenericPacket::parse_body: {:?} {} {}", "GenericPacket::parse_body: {:?} {} {}",
client_state, client_state,
@ -77,7 +77,7 @@ macro_rules! generic_packet {
#[tracing::instrument] #[tracing::instrument]
pub fn serialize(&self) -> (crate::packets::PacketId, Vec<u8>) { pub fn serialize(&self) -> (crate::packets::PacketId, Vec<u8>) {
use composition_parsing::Parsable; use composition_parsing::parsable::Parsable;
tracing::trace!("GenericPacket::serialize: {:?}", self); tracing::trace!("GenericPacket::serialize: {:?}", self);
match self { match self {
$( $(
@ -145,7 +145,7 @@ macro_rules! packet {
const CLIENT_STATE: crate::ClientState = $client_state; const CLIENT_STATE: crate::ClientState = $client_state;
const IS_SERVERBOUND: bool = $serverbound; const IS_SERVERBOUND: bool = $serverbound;
} }
impl composition_parsing::Parsable for $packet_type { impl composition_parsing::parsable::Parsable for $packet_type {
#[tracing::instrument] #[tracing::instrument]
fn parse<'data>(data: &'data [u8]) -> composition_parsing::ParseResult<'_, Self> { fn parse<'data>(data: &'data [u8]) -> composition_parsing::ParseResult<'_, Self> {
$parse_body(data) $parse_body(data)

View File

@ -1,6 +1,10 @@
/// Packets for the `ClientState::Handshake` state.
pub mod handshake; pub mod handshake;
/// Packets for the `ClientState::Login` state.
pub mod login; pub mod login;
/// Packets for the `ClientState::Play` state.
pub mod play; pub mod play;
/// Packets for the `ClientState::Status` state.
pub mod status; pub mod status;
pub use handshake::*; pub use handshake::*;

View File

@ -1,13 +1,14 @@
[package] [package]
name = "composition-world" name = "composition-world"
version = "0.1.0"
edition = "2021"
authors = ["Garen Tyler <garentyler@garen.dev>"]
description = "A Minecraft world generator implementation that allows for custom worlds" 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] [dependencies]
anyhow = { workspace = true } anyhow.workspace = true
async-trait = { workspace = true } async-trait.workspace = true
composition-protocol = { workspace = true } composition-protocol.workspace = true
thiserror = { workspace = true } thiserror.workspace = true

View File

@ -4,6 +4,8 @@ use crate::{
}; };
use std::collections::HashMap; use std::collections::HashMap;
/// `Chunk`s divide the world into smaller parts
/// and manage the blocks and entities within.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Chunk { pub struct Chunk {
// blocks[x][y][z] // 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)] #[derive(Debug, Copy, Clone, PartialEq, Default, Eq, Hash)]
pub struct ChunkPosition { pub struct ChunkPosition {
pub x: i32, pub x: i32,
pub y: i32,
pub z: i32, pub z: i32,
} }
impl From<BlockPosition> for ChunkPosition { impl From<BlockPosition> for ChunkPosition {
@ -29,6 +35,7 @@ impl From<BlockPosition> for ChunkPosition {
// Divide by 16 to get the chunk. // Divide by 16 to get the chunk.
ChunkPosition { ChunkPosition {
x: value.x >> 4, x: value.x >> 4,
y: value.y >> 4,
z: value.z >> 4, z: value.z >> 4,
} }
} }
@ -38,6 +45,7 @@ impl From<EntityPosition> for ChunkPosition {
// Divide by 16 and convert to i32. // Divide by 16 and convert to i32.
ChunkPosition { ChunkPosition {
x: (value.x / 16.0) as i32, x: (value.x / 16.0) as i32,
y: (value.y / 16.0) as i32,
z: (value.z / 16.0) as i32, z: (value.z / 16.0) as i32,
} }
} }

View File

@ -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<T> = std::result::Result<T, Error>;

View File

@ -1 +1,2 @@
/// An implementation of Minecraft's superflat world type.
pub mod superflat;

View File

@ -0,0 +1,4 @@
/// An implementation of Minecraft's superflat world type.
pub struct Superflat;
// TODO: Implement superflat world.

View File

@ -1,56 +1,70 @@
#![deny(clippy::all)] /// Worlds are divided into chunks.
pub mod chunks; pub mod chunks;
/// When managing a `World` encounters errors.
pub mod error;
/// Default implementations of `World`, such as `Superflat`.
pub mod generators; pub mod generators;
/// Useful re-exports.
pub mod prelude {
pub use crate::{chunks::Chunk, World};
}
pub use composition_protocol::{blocks, entities}; pub use composition_protocol::{blocks, entities};
pub use error::{Error, Result};
use crate::chunks::ChunkPosition; use crate::chunks::{Chunk, ChunkPosition};
use blocks::BlockPosition; use blocks::{Block, BlockPosition};
use entities::{Entity, EntityId, EntityPosition};
use std::path::Path; use std::path::Path;
use thiserror::Error;
/// A `World` abstracts away world generation, updating blocks, and saving.
#[async_trait::async_trait] #[async_trait::async_trait]
pub trait World { pub trait World {
/// Get the world's name. /// Get the world's name.
fn name() -> String; fn name() -> String;
/// Create a new world. /// Create a new world from a seed.
fn new(seed: u128) -> Self; fn new(seed: u128) -> Self;
/// Load an existing world. /// Load an existing world from a directory.
async fn load_from_dir<P: AsRef<Path> + Send>(world_dir: P) -> Result<Self> async fn load<P: AsRef<Path> + Send>(world_dir: P) -> Result<Self>
where where
Self: Sized; Self: Sized;
/// Save the world to a directory. /// Save the world to a directory.
async fn save_to_dir<P: AsRef<Path> + Send>(&self, world_dir: P) -> Result<()>; async fn save<P: AsRef<Path> + 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<()>; 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 unload_chunk(&self, chunk_pos: ChunkPosition) -> Result<()>;
async fn get_chunk(&self, chunk_pos: ChunkPosition) -> Result<chunks::Chunk>; /// Gets a copy of the chunk at the given `ChunkPosition`.
async fn set_chunk(&self, chunk_pos: ChunkPosition, chunk: chunks::Chunk) -> Result<()>; async fn get_chunk(&self, chunk_pos: ChunkPosition) -> Result<Chunk>;
/// 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. /// Get the block at the given `BlockPosition`.
async fn get_block(&self, block_pos: BlockPosition) -> Result<blocks::Block>; ///
async fn set_block(&self, block_pos: BlockPosition, block: blocks::Block) -> Result<()>; /// Async because the containing chunk might need to be loaded.
async fn get_block(&self, block_pos: BlockPosition) -> Result<Block>;
/// 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. /// Spawn an entity at the given `EntityPosition`.
async fn spawn_entity( ///
&self, /// Async because the containing chunk might need to be loaded.
entity_pos: entities::EntityPosition, async fn spawn_entity(&self, entity_pos: EntityPosition, entity: Entity) -> Result<EntityId>;
entity: entities::Entity, /// Get a reference to the entity with the given `EntityId`.
) -> Result<entities::EntityId>; /// Returns Err if no entity could be found with that id.
fn get_entity(&self, entity_id: entities::EntityId) -> Result<&entities::Entity>; fn get_entity(&self, entity_id: EntityId) -> Result<&Entity>;
fn get_entity_mut(&self, entity_id: entities::EntityId) -> Result<&mut entities::Entity>; /// Get a mutable reference to the entity with the given `EntityId`.
async fn remove_entity(&self, entity_id: entities::EntityId) -> Result<()>; /// 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<T> = std::result::Result<T, WorldError>;

View File

@ -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<Instant> = 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<T> = Result<(T, usize), ParseError>;
}

View File

@ -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.
}