Write documentation and convert to virtual workspace
This commit is contained in:
parent
e5cb669c73
commit
5dc9d3bb8a
14
.github/workflows/clippy.yml
vendored
14
.github/workflows/clippy.yml
vendored
@ -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
11
Cargo.lock
generated
@ -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",
|
||||||
|
43
Cargo.toml
43
Cargo.toml
@ -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"
|
||||||
|
33
crates/composition-core/Cargo.toml
Normal file
33
crates/composition-core/Cargo.toml
Normal 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"
|
@ -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,
|
||||||
}
|
}
|
10
crates/composition-core/src/error.rs
Normal file
10
crates/composition-core/src/error.rs
Normal 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>;
|
23
crates/composition-core/src/lib.rs
Normal file
23
crates/composition-core/src/lib.rs
Normal 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
|
||||||
|
}
|
@ -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.
|
@ -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!"
|
||||||
}));
|
}));
|
||||||
|
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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
|
||||||
|
@ -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)>;
|
||||||
|
@ -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::*;
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
@ -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;
|
||||||
|
26
crates/composition-protocol/src/error.rs
Normal file
26
crates/composition-protocol/src/error.rs
Normal 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>;
|
@ -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>;
|
|
||||||
|
@ -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,
|
||||||
|
@ -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 {
|
||||||
|
@ -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::*;
|
||||||
|
@ -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)
|
||||||
|
@ -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::*;
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
13
crates/composition-world/src/error.rs
Normal file
13
crates/composition-world/src/error.rs
Normal 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>;
|
@ -1 +1,2 @@
|
|||||||
|
/// An implementation of Minecraft's superflat world type.
|
||||||
|
pub mod superflat;
|
||||||
|
4
crates/composition-world/src/generators/superflat.rs
Normal file
4
crates/composition-world/src/generators/superflat.rs
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
/// An implementation of Minecraft's superflat world type.
|
||||||
|
pub struct Superflat;
|
||||||
|
|
||||||
|
// TODO: Implement superflat world.
|
@ -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>;
|
|
||||||
|
34
src/lib.rs
34
src/lib.rs
@ -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>;
|
|
||||||
}
|
|
@ -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.
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user