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]
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
- uses: actions/checkout@v3
- name: Run Clippy
run: cargo clippy --all-targets --all-features

11
Cargo.lock generated
View File

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

View File

@ -1,48 +1,29 @@
[workspace]
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]
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 <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:
# async-trait = "0.1.48"
# 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();
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]
fn read_file(path: &Path) -> std::io::Result<Vec<u8>> {
trace!("{:?}", path);
@ -23,6 +25,7 @@ fn read_file(path: &Path) -> std::io::Result<Vec<u8>> {
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<tracing::Level>,
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_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.

View File

@ -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<SL00LoginStart>),
},
/// 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<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)]
pub struct NetworkClient {
pub(crate) struct NetworkClient {
/// The `NetworkClient`'s unique id.
pub id: u128,
pub state: NetworkClientState,
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>,
/// 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>,
/// 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<GenericPacket>,
}
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<P: std::fmt::Debug + TryFrom<GenericPacket>>(
&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 Ok(packet) = TryInto::<P>::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<composition_protocol::mctypes::Chat>) {
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!"
}));

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::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<T> = std::result::Result<T, ServerError>;
/// The main state and logic of the program.
#[derive(Debug)]
pub struct Server {
clients: Arc<RwLock<Vec<NetworkClient>>>,
net_tasks_handle: JoinHandle<()>,
}
impl Server {
#[tracing::instrument]
pub async fn new<A: 'static + ToSocketAddrs + Send + std::fmt::Debug>(
@ -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;
}
}

View File

@ -1,13 +1,14 @@
[package]
name = "composition-parsing"
version = "0.1.0"
edition = "2021"
authors = ["Garen Tyler <garentyler@garen.dev>"]
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

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)]
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<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)>;

View File

@ -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::*;

View File

@ -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<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>>
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<Self>>
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<Self>>
where
Self: Sized,

View File

@ -1,19 +1,20 @@
[package]
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"
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

View File

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

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;
/// 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<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 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,

View File

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

View File

@ -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::*;

View File

@ -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<GenericPacket>
+ Into<GenericPacket>
+ composition_parsing::Parsable
std::fmt::Debug + Clone + TryFrom<GenericPacket> + Into<GenericPacket> + 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<u8>) {
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)

View File

@ -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::*;

View File

@ -1,13 +1,14 @@
[package]
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"
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

View File

@ -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<BlockPosition> for ChunkPosition {
@ -29,6 +35,7 @@ impl From<BlockPosition> 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<EntityPosition> 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,
}
}

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;
/// 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<P: AsRef<Path> + Send>(world_dir: P) -> Result<Self>
/// Load an existing world from a directory.
async fn load<P: AsRef<Path> + Send>(world_dir: P) -> Result<Self>
where
Self: Sized;
/// 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<()>;
/// 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<chunks::Chunk>;
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<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.
async fn get_block(&self, block_pos: BlockPosition) -> Result<blocks::Block>;
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<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.
async fn spawn_entity(
&self,
entity_pos: entities::EntityPosition,
entity: entities::Entity,
) -> Result<entities::EntityId>;
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<EntityId>;
/// 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<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.
}