From ca94ec10f24ea60b3c61aaee9b6e14e8feee1540 Mon Sep 17 00:00:00 2001 From: Zoey Date: Thu, 18 Apr 2024 19:43:27 -0700 Subject: [PATCH] initial commit --- .gitignore | 1 + .rustfmt.toml | 1 + Cargo.lock | 514 ++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 11 + src/level.rs | 41 ++++ src/main.rs | 15 ++ src/packet.rs | 159 +++++++++++++ src/packet/client.rs | 133 +++++++++++ src/packet/server.rs | 262 +++++++++++++++++++++ src/player.rs | 61 +++++ src/server.rs | 82 +++++++ src/server/network.rs | 344 ++++++++++++++++++++++++++++ 12 files changed, 1624 insertions(+) create mode 100644 .gitignore create mode 100644 .rustfmt.toml create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 src/level.rs create mode 100644 src/main.rs create mode 100644 src/packet.rs create mode 100644 src/packet/client.rs create mode 100644 src/packet/server.rs create mode 100644 src/player.rs create mode 100644 src/server.rs create mode 100644 src/server/network.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/.rustfmt.toml b/.rustfmt.toml new file mode 100644 index 0000000..218e203 --- /dev/null +++ b/.rustfmt.toml @@ -0,0 +1 @@ +hard_tabs = true diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..29c0d30 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,514 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "autocfg" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" + +[[package]] +name = "backtrace" +version = "0.3.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bytes" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" + +[[package]] +name = "cc" +version = "1.0.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17f6e324229dc011159fcc089755d1e2e216a90d43a7dea6853ca740b84f35e7" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "classics" +version = "0.1.0" +dependencies = [ + "flate2", + "half", + "parking_lot", + "rand", + "tokio", +] + +[[package]] +name = "crc32fast" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + +[[package]] +name = "flate2" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "getrandom" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gimli" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" + +[[package]] +name = "half" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888" +dependencies = [ + "cfg-if", + "crunchy", +] + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "libc" +version = "0.2.153" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" + +[[package]] +name = "lock_api" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "memchr" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" + +[[package]] +name = "miniz_oxide" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.48.0", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "object" +version = "0.32.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +dependencies = [ + "memchr", +] + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.48.5", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "proc-macro2" +version = "1.0.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "socket2" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "syn" +version = "2.0.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tokio" +version = "1.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "num_cpus", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.48.0", +] + +[[package]] +name = "tokio-macros" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.5", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" +dependencies = [ + "windows_aarch64_gnullvm 0.52.5", + "windows_aarch64_msvc 0.52.5", + "windows_i686_gnu 0.52.5", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.5", + "windows_x86_64_gnu 0.52.5", + "windows_x86_64_gnullvm 0.52.5", + "windows_x86_64_msvc 0.52.5", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..61c53b1 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,11 @@ +[package] +edition = "2021" +name = "classics" +version = "0.1.0" + +[dependencies] +flate2 = "1" +half = "2" +parking_lot = "0.12.1" +rand = "0.8" +tokio = {version = "1", features = ["full"]} diff --git a/src/level.rs b/src/level.rs new file mode 100644 index 0000000..ec353da --- /dev/null +++ b/src/level.rs @@ -0,0 +1,41 @@ +/// a classic level +#[derive(Debug, Clone)] +pub struct Level { + /// the size of the level in the X direction + pub x_size: usize, + /// the size of the level in the Y direction + pub y_size: usize, + /// the size of the level in the Z direction + pub z_size: usize, + + /// the blocks which make up the level + pub blocks: Vec, +} + +impl Level { + /// creates a new level with the given dimensions + pub fn new(x_size: usize, y_size: usize, z_size: usize) -> Self { + Self { + x_size, + y_size, + z_size, + blocks: vec![0; x_size * y_size * z_size], + } + } + + /// gets the index for a given block position + pub fn index(&self, x: usize, y: usize, z: usize) -> usize { + x + z * self.x_size + y * self.x_size * self.z_size + } + + /// gets the block at the given position + pub fn get_block(&self, x: usize, y: usize, z: usize) -> u8 { + self.blocks[self.index(x, y, z)] + } + + /// sets the block at the given position + pub fn set_block(&mut self, x: usize, y: usize, z: usize, block: u8) { + let index = self.index(x, y, z); + self.blocks[index] = block; + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..1b86db4 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,15 @@ +use server::Server; + +mod level; +mod packet; +mod player; +mod server; + +#[tokio::main] +async fn main() -> std::io::Result<()> { + let mut server = Server::new().await?; + + server.run().await?; + + Ok(()) +} diff --git a/src/packet.rs b/src/packet.rs new file mode 100644 index 0000000..bdb1d58 --- /dev/null +++ b/src/packet.rs @@ -0,0 +1,159 @@ +use half::f16; + +pub mod client; +pub mod server; + +/// length of classic strings +pub const STRING_LENGTH: usize = 64; +/// length of classic level chunk arrays +pub const ARRAY_LENGTH: usize = 1024; +/// units in an f16 unit +pub const F16_UNITS: f32 = 32.0; + +/// helper for reading packets +#[derive(Debug)] +pub struct PacketReader<'p> { + raw_packet: &'p [u8], + cursor: usize, +} + +impl<'p> PacketReader<'p> { + /// creates a new packet reader from the given packet data + pub fn new(raw_packet: &'p [u8]) -> Self { + Self { + raw_packet, + cursor: 0, + } + } + + /// gets the next u8 in the packet, if any + fn next_u8(&mut self) -> Option { + let r = self.raw_packet.get(self.cursor).copied(); + self.cursor = self.cursor.checked_add(1).unwrap_or(self.cursor); + r + } + + /// gets the next i8 in the packet, if any + fn next_i8(&mut self) -> Option { + self.next_u8().map(|b| b as i8) + } + + /// gets the next u16 in the packet, if any + fn next_u16(&mut self) -> Option { + Some(u16::from_be_bytes([self.next_u8()?, self.next_u8()?])) + } + + /// gets the next i16 in the packet, if any + fn next_i16(&mut self) -> Option { + self.next_u16().map(|s| s as i16) + } + + /// gets the next f16 in the packet, if any + fn next_f16(&mut self) -> Option { + self.next_i16().map(|v| f16::from_f32(v as f32 / F16_UNITS)) + } + + /// gets the next string in the packet, if any + fn next_string(&mut self) -> Option { + let mut chars: Vec = Vec::new(); + for _ in 0..STRING_LENGTH { + chars.push(self.next_u8()? as char); + } + Some(String::from_iter(chars).trim().to_string()) + } + + /// gets the next array of the given length in the packet, if any + fn next_array_of_length(&mut self, len: usize) -> Option> { + let mut bytes: Vec = Vec::new(); + let mut append = true; + for _ in 0..len { + let b = self.next_u8()?; + if append { + if b == 0 { + append = false; + } else { + bytes.push(b); + } + } + } + Some(bytes) + } + + /// gets the next array of default size in the packet, if any + fn next_array(&mut self) -> Option> { + self.next_array_of_length(ARRAY_LENGTH) + } +} + +/// helper for writing a packet +#[derive(Debug, Default)] +pub struct PacketWriter { + raw_packet: Vec, +} + +impl PacketWriter { + /// gets the actual raw packet data from the writer + pub fn into_raw_packet(self) -> Vec { + self.raw_packet + } + + /// writes a u8 to the packet + pub fn write_u8(mut self, b: u8) -> Self { + self.raw_packet.push(b); + self + } + + /// writes an i8 to the packet + fn write_i8(self, b: i8) -> Self { + self.write_u8(b as u8) + } + + /// writes a u16 to the packet + fn write_u16(self, sh: u16) -> Self { + let mut s = self; + for b in sh.to_be_bytes() { + s = s.write_u8(b); + } + s + } + + /// writes an i16 to the packet + fn write_i16(self, sh: i16) -> Self { + self.write_u16(sh as u16) + } + + /// writes an f16 to the packet + fn write_f16(self, f: f16) -> Self { + let r = (f.to_f32() * F16_UNITS) as i16; + self.write_i16(r) + } + + /// writes a string to the packet + fn write_string(self, str: &str) -> Self { + let mut s = self; + for b in str + .as_bytes() + .iter() + .copied() + .chain(Some(0x20).into_iter().cycle()) + .take(STRING_LENGTH) + { + s = s.write_u8(b); + } + s + } + + /// writes an array of the given length to the packet + fn write_array_of_length(self, bytes: &[u8], len: usize) -> Self { + let mut s = self; + for i in 0..len { + s = s.write_u8(bytes.get(i).copied().unwrap_or_default()); + } + s + } + + /// writes an array of default length to the packet + fn write_array(self, bytes: &[u8]) -> Self { + self.write_array_of_length(bytes, ARRAY_LENGTH) + } +} diff --git a/src/packet/client.rs b/src/packet/client.rs new file mode 100644 index 0000000..773b89c --- /dev/null +++ b/src/packet/client.rs @@ -0,0 +1,133 @@ +use half::f16; + +/// enum for a packet which can be received by the client +#[derive(Debug, Clone)] +pub enum ClientPacket { + /// packet sent by a client to identify itself to the server + PlayerIdentification { + /// should always be 0x07 for classic clients >= 0.28 + protocol_version: u8, + username: String, + /// currently unverified, original minecraft auth for classic is gone anyway + /// TODO: use verification key field as password protection? investigate + verification_key: String, + _unused: u8, + }, + /// packet sent when a client changes a block + /// because changes are reflected immediately, to restrict changes, server must send back its own SetBlock packet with the original block + SetBlock { + x: i16, + y: i16, + z: i16, + /// 0x00 for destroy, 0x01 for create + mode: u8, + block_type: u8, + }, + /// sent to update the player's current position and orientation with the server + PositionOrientation { + /// should always be 0xff (-1), referring to the player who sent it + _player_id: i8, + x: f16, + y: f16, + z: f16, + yaw: u8, + pitch: u8, + }, + /// packet for the client to send chat messages + Message { + /// should always be 0xff (-1), referring to the player who sent it + player_id: i8, + message: String, + }, +} + +impl ClientPacket { + // unused currently, so disabled + // /// gets the packet's id + // pub fn get_id(&self) -> u8 { + // match self { + // Self::PlayerIdentification { .. } => 0x00, + // Self::SetBlock { .. } => 0x05, + // Self::PositionOrientation { .. } => 0x08, + // Self::Message { .. } => 0x0d, + // } + // } + + /// reads the packet + pub fn read(id: u8, packet: &mut super::PacketReader) -> Option { + Some(match id { + 0x00 => Self::PlayerIdentification { + protocol_version: packet.next_u8()?, + username: packet.next_string()?, + verification_key: packet.next_string()?, + _unused: packet.next_u8()?, + }, + 0x05 => Self::SetBlock { + x: packet.next_i16()?, + y: packet.next_i16()?, + z: packet.next_i16()?, + mode: packet.next_u8()?, + block_type: packet.next_u8()?, + }, + 0x08 => Self::PositionOrientation { + _player_id: packet.next_i8()?, + x: packet.next_f16()?, + y: packet.next_f16()?, + z: packet.next_f16()?, + yaw: packet.next_u8()?, + pitch: packet.next_u8()?, + }, + 0x0d => Self::Message { + player_id: packet.next_i8()?, + message: packet.next_string()?, + }, + _ => return None, + }) + } + + // only needed on the client, so disabled for now + // /// writes the packet + // pub fn write(&self, writer: super::PacketWriter) -> super::PacketWriter { + // match self { + // Self::PlayerIdentification { + // protocol_version, + // username, + // verification_key, + // _unused, + // } => writer + // .write_u8(*protocol_version) + // .write_string(username) + // .write_string(verification_key) + // .write_u8(*_unused), + // Self::SetBlock { + // x, + // y, + // z, + // mode, + // block_type, + // } => writer + // .write_i16(*x) + // .write_i16(*y) + // .write_i16(*z) + // .write_u8(*mode) + // .write_u8(*block_type), + // Self::PositionOrientation { + // player_id, + // x, + // y, + // z, + // yaw, + // pitch, + // } => writer + // .write_i8(*player_id) + // .write_f16(*x) + // .write_f16(*y) + // .write_f16(*z) + // .write_u8(*yaw) + // .write_u8(*pitch), + // Self::Message { player_id, message } => { + // writer.write_i8(*player_id).write_string(message) + // } + // } + // } +} diff --git a/src/packet/server.rs b/src/packet/server.rs new file mode 100644 index 0000000..9991c68 --- /dev/null +++ b/src/packet/server.rs @@ -0,0 +1,262 @@ +use half::f16; + +use crate::player::PlayerType; + +#[derive(Debug, Clone)] +#[allow(unused)] +pub enum ServerPacket { + /// packet sent as a response to joining clients + ServerIdentification { + /// should be 0x07 + protocol_version: u8, + server_name: String, + server_motd: String, + user_type: PlayerType, + }, + /// since clients do not notify the server when leaving, the ping packet is used to check if the client is still connected + /// TODO: implement pinging? classicube works fine without it + Ping {}, + /// informs clients that there is incoming level data + LevelInitialize {}, + /// packet to send a chunk (not minecraft chunk) of gzipped level data + LevelDataChunk { + chunk_length: i16, + chunk_data: Vec, + percent_complete: u8, + }, + /// packet sent after chunk data is finished sending containing the level dimensions + LevelFinalize { + x_size: i16, + y_size: i16, + z_size: i16, + }, + + /// indicates a block change + /// when a player changes a block, their own change is echoed back to them + SetBlock { + x: i16, + y: i16, + z: i16, + block_type: u8, + }, + /// packet sent when a new player spawns + /// also contains their spawn point + SpawnPlayer { + player_id: i8, + player_name: String, + x: f16, + y: f16, + z: f16, + yaw: u8, + pitch: u8, + }, + /// packet to set a player's position and orientation + SetPositionOrientation { + player_id: i8, + x: f16, + y: f16, + z: f16, + yaw: u8, + pitch: u8, + }, + /// packet to update a player's position and orientation + /// TODO: implement? + UpdatePositionOrientation { + player_id: i8, + x_change: f16, + y_change: f16, + z_change: f16, + yaw: u8, + pitch: u8, + }, + /// packet to update a player's position + /// TODO: implement? + UpdatePosition { + player_id: i8, + x_change: f16, + y_change: f16, + z_change: f16, + }, + /// packet to update a player's orientation + /// TODO: implement? + UpdateOrientation { player_id: i8, yaw: u8, pitch: u8 }, + /// packet sent when a player is despawned from the world (i.e. when leaving) + DespawnPlayer { player_id: i8 }, + /// packet sent when there's a chat message to go out + Message { player_id: i8, message: String }, + /// informs a client that they're being disconnected from the server and why + DisconnectPlayer { disconnect_reason: String }, + /// packet sent to a user to inform them that their user type has changed + UpdateUserType { + /// 0x00 for normal, 0x64 for op + user_type: PlayerType, + }, +} + +impl ServerPacket { + /// gets the packet's id + pub fn get_id(&self) -> u8 { + match self { + Self::ServerIdentification { .. } => 0x00, + Self::Ping {} => 0x01, + Self::LevelInitialize {} => 0x02, + Self::LevelDataChunk { .. } => 0x03, + Self::LevelFinalize { .. } => 0x04, + Self::SetBlock { .. } => 0x06, + Self::SpawnPlayer { .. } => 0x07, + Self::SetPositionOrientation { .. } => 0x08, + Self::UpdatePositionOrientation { .. } => 0x09, + Self::UpdatePosition { .. } => 0x0a, + Self::UpdateOrientation { .. } => 0x0b, + Self::DespawnPlayer { .. } => 0x0c, + Self::Message { .. } => 0x0d, + Self::DisconnectPlayer { .. } => 0x0e, + Self::UpdateUserType { .. } => 0x0f, + } + } + + /// writes the packet + pub fn write(&self, writer: super::PacketWriter) -> super::PacketWriter { + match self { + Self::ServerIdentification { + protocol_version, + server_name, + server_motd, + user_type, + } => writer + .write_u8(*protocol_version) + .write_string(server_name) + .write_string(server_motd) + .write_u8(*user_type as u8), + Self::Ping {} => writer, + Self::LevelInitialize {} => writer, + Self::LevelDataChunk { + chunk_length, + chunk_data, + percent_complete, + } => writer + .write_i16(*chunk_length) + .write_array(chunk_data) + .write_u8(*percent_complete), + Self::LevelFinalize { + x_size, + y_size, + z_size, + } => writer + .write_i16(*x_size) + .write_i16(*y_size) + .write_i16(*z_size), + Self::SetBlock { + x, + y, + z, + block_type, + } => writer + .write_i16(*x) + .write_i16(*y) + .write_i16(*z) + .write_u8(*block_type), + Self::SpawnPlayer { + player_id, + player_name, + x, + y, + z, + yaw, + pitch, + } => writer + .write_i8(*player_id) + .write_string(player_name) + .write_f16(*x) + .write_f16(*y) + .write_f16(*z) + .write_u8(*yaw) + .write_u8(*pitch), + Self::SetPositionOrientation { + player_id, + x, + y, + z, + yaw, + pitch, + } => writer + .write_i8(*player_id) + .write_f16(*x) + .write_f16(*y) + .write_f16(*z) + .write_u8(*yaw) + .write_u8(*pitch), + Self::UpdatePositionOrientation { + player_id, + x_change, + y_change, + z_change, + yaw, + pitch, + } => writer + .write_i8(*player_id) + .write_f16(*x_change) + .write_f16(*y_change) + .write_f16(*z_change) + .write_u8(*yaw) + .write_u8(*pitch), + Self::UpdatePosition { + player_id, + x_change, + y_change, + z_change, + } => writer + .write_i8(*player_id) + .write_f16(*x_change) + .write_f16(*y_change) + .write_f16(*z_change), + Self::UpdateOrientation { + player_id, + yaw, + pitch, + } => writer.write_i8(*player_id).write_u8(*yaw).write_u8(*pitch), + Self::DespawnPlayer { player_id } => writer.write_i8(*player_id), + Self::Message { player_id, message } => { + writer.write_i8(*player_id).write_string(message) + } + Self::DisconnectPlayer { disconnect_reason } => writer.write_string(disconnect_reason), + Self::UpdateUserType { user_type } => writer.write_u8(*user_type as u8), + } + } + + /// gets the player id contained in the packet, if any + pub fn get_player_id(&self) -> Option { + Some(match self { + Self::SpawnPlayer { player_id, .. } => *player_id, + Self::SetPositionOrientation { player_id, .. } => *player_id, + Self::UpdatePositionOrientation { player_id, .. } => *player_id, + Self::UpdatePosition { player_id, .. } => *player_id, + Self::UpdateOrientation { player_id, .. } => *player_id, + Self::DespawnPlayer { player_id, .. } => *player_id, + Self::Message { player_id, .. } => *player_id, + _ => return None, + }) + } + + /// sets the player id in the packet if possible + pub fn set_player_id(&mut self, new_player_id: i8) { + match self { + Self::SpawnPlayer { player_id, .. } => *player_id = new_player_id, + Self::SetPositionOrientation { player_id, .. } => *player_id = new_player_id, + Self::UpdatePositionOrientation { player_id, .. } => *player_id = new_player_id, + Self::UpdatePosition { player_id, .. } => *player_id = new_player_id, + Self::UpdateOrientation { player_id, .. } => *player_id = new_player_id, + Self::DespawnPlayer { player_id, .. } => *player_id = new_player_id, + Self::Message { player_id, .. } => *player_id = new_player_id, + _ => {} + } + } + + /// gets whether this packet should echo back to the current player + pub fn should_echo(&self) -> bool { + matches!( + self, + Self::SetBlock { .. } | Self::SpawnPlayer { .. } | Self::Message { .. } + ) + } +} diff --git a/src/player.rs b/src/player.rs new file mode 100644 index 0000000..7ad5a6a --- /dev/null +++ b/src/player.rs @@ -0,0 +1,61 @@ +use std::net::SocketAddr; + +use half::f16; + +use crate::packet::server::ServerPacket; + +/// struct for players +#[derive(Debug)] +pub struct Player { + /// the player's id + pub id: i8, + /// the player's username + pub username: String, + /// the player's X coordinate + pub x: f16, + /// the player's Y coordinate + pub y: f16, + /// the player's Z coordinate + pub z: f16, + /// the player's yaw + pub yaw: u8, + /// the player's pitch + pub pitch: u8, + /// the player's permission state + pub player_type: PlayerType, + + /// the player's IP address + pub _addr: SocketAddr, + /// queue of packets to be sent to this player + pub packets_to_send: Vec, +} + +/// enum describing types of players +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u8)] +pub enum PlayerType { + /// a normal player + Normal = 0x00, + /// a player who's an operator + Operator = 0x64, +} + +impl Default for PlayerType { + fn default() -> Self { + Self::Normal + } +} + +impl TryFrom for PlayerType { + type Error = (); + + fn try_from(value: u8) -> Result { + if value == Self::Normal as u8 { + Ok(Self::Normal) + } else if value == Self::Operator as u8 { + Ok(Self::Operator) + } else { + Err(()) + } + } +} diff --git a/src/server.rs b/src/server.rs new file mode 100644 index 0000000..64b0846 --- /dev/null +++ b/src/server.rs @@ -0,0 +1,82 @@ +mod network; + +use std::sync::Arc; + +// use parking_lot::RwLock; +use rand::Rng; +use tokio::{net::TcpListener, sync::RwLock}; + +use crate::{level::Level, player::Player}; + +const DEFAULT_SERVER_SIZE: usize = 128; + +/// the server +#[derive(Debug)] +pub struct Server { + /// shared server data + pub data: Arc>, + /// the server's listener + pub listener: TcpListener, +} + +/// shared server data +#[derive(Debug)] +pub struct ServerData { + /// the level + pub level: Level, + /// list of players connected to the server + pub players: Vec, + /// list of player ids which have been freed up + pub free_player_ids: Vec, +} + +impl Server { + /// creates a new server with a generated level + pub async fn new() -> std::io::Result { + println!("generating level"); + let mut rng = rand::thread_rng(); + let mut level = Level::new( + DEFAULT_SERVER_SIZE, + DEFAULT_SERVER_SIZE, + DEFAULT_SERVER_SIZE, + ); + for x in 0..level.x_size { + for y in 0..(level.y_size / 2) { + for z in 0..level.z_size { + level.set_block(x, y, z, rng.gen_range(0..50)); + } + } + } + println!("done!"); + + Self::new_with_level(level).await + } + + /// creates a new server with the given level + pub async fn new_with_level(level: Level) -> std::io::Result { + let listener = TcpListener::bind("127.0.0.1:25565").await?; + + Ok(Self { + data: Arc::new(RwLock::new(ServerData { + level, + players: Default::default(), + free_player_ids: Vec::new(), + })), + listener, + }) + } + + /// starts the server + pub async fn run(&mut self) -> std::io::Result<()> { + loop { + let (stream, addr) = self.listener.accept().await?; + println!("connection from {addr}"); + let data = self.data.clone(); + tokio::spawn(async move { + network::handle_stream(stream, addr, data) + .await + .expect("failed to handle client stream"); + }); + } + } +} diff --git a/src/server/network.rs b/src/server/network.rs new file mode 100644 index 0000000..7eb5d1d --- /dev/null +++ b/src/server/network.rs @@ -0,0 +1,344 @@ +use std::{collections::VecDeque, io::Write, net::SocketAddr, sync::Arc}; + +use flate2::{write::GzEncoder, Compression}; +use half::f16; +use tokio::{ + io::{AsyncWriteExt, Interest}, + net::TcpStream, + sync::RwLock, +}; + +use crate::{ + level::Level, + packet::{ + client::ClientPacket, server::ServerPacket, PacketReader, PacketWriter, ARRAY_LENGTH, + }, + player::{Player, PlayerType}, +}; + +use super::ServerData; + +pub(super) async fn handle_stream( + mut stream: TcpStream, + addr: SocketAddr, + data: Arc>, +) -> std::io::Result<()> { + let mut own_id: i8 = -1; + let r = handle_stream_inner(&mut stream, addr, data.clone(), &mut own_id).await; + + match r { + Ok(disconnect_reason) => { + if let Some(disconnect_reason) = disconnect_reason { + let packet = ServerPacket::DisconnectPlayer { disconnect_reason }; + let writer = PacketWriter::default().write_u8(packet.get_id()); + let msg = packet.write(writer).into_raw_packet(); + if let Err(e) = stream.write_all(&msg).await { + eprintln!("Failed to write disconnect packet for <{addr}>: {e}"); + } + } + } + Err(e) => eprintln!("Error in stream handler for <{addr}>: {e}"), + } + + if let Err(e) = stream.shutdown().await { + eprintln!("Failed to properly shut down stream for <{addr}>: {e}"); + } + + let mut data = data.write().await; + if let Some(index) = data.players.iter().position(|p| p.id == own_id) { + let player = data.players.remove(index); + + let despawn_packet = ServerPacket::DespawnPlayer { player_id: own_id }; + let message_packet = ServerPacket::Message { + player_id: own_id, + message: format!("&e{} has left the server.", player.username), + }; + for player in &mut data.players { + player.packets_to_send.push(despawn_packet.clone()); + player.packets_to_send.push(message_packet.clone()); + } + } + + Ok(()) +} + +async fn handle_stream_inner( + stream: &mut TcpStream, + addr: SocketAddr, + data: Arc>, + own_id: &mut i8, +) -> std::io::Result> { + const BUF_SIZE: usize = 130; + + let mut reply_queue: VecDeque = VecDeque::new(); + let mut packet_buf = [0u8]; + let mut read_buf; + + loop { + let ready = stream + .ready(Interest::READABLE | Interest::WRITABLE) + .await?; + + if ready.is_read_closed() { + println!("disconnecting {addr}"); + break; + } + + if ready.is_readable() { + match stream.try_read(&mut packet_buf) { + Ok(n) => { + if n == 1 { + read_buf = [0; BUF_SIZE]; + match stream.try_read(&mut read_buf) { + Ok(_n) => {} + Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => continue, + Err(e) => return Err(e), + } + + let mut reader = PacketReader::new(&read_buf); + + if let Some(packet) = ClientPacket::read(packet_buf[0], &mut reader) { + // println!("{packet:#?}"); + match packet { + ClientPacket::PlayerIdentification { + protocol_version, + username, + verification_key: _, + _unused, + } => { + if protocol_version != 0x07 { + return Ok(Some("Unknown protocol version! Please connect with a classic 0.30-compatible client.".to_string())); + } + + let zero = f16::from_f32(0.0); + + let mut data = data.write().await; + + for player in &data.players { + if player.username == username { + return Ok(Some( + "Player with username already connected!" + .to_string(), + )); + } + } + + *own_id = data + .free_player_ids + .pop() + .unwrap_or_else(|| data.players.len() as i8); + + let player = Player { + _addr: addr, + id: *own_id, // TODO: actually assign user ids + username, + // TODO: properly assign spawn stuff + x: zero, + y: zero, + z: zero, + yaw: 0, + pitch: 0, + player_type: PlayerType::Normal, + packets_to_send: Vec::new(), + }; + + reply_queue.push_back(ServerPacket::ServerIdentification { + protocol_version: 0x07, + server_name: "test server".to_string(), + server_motd: "whoaaaaaa".to_string(), + user_type: PlayerType::Normal, + }); + + println!("generating level packets"); + reply_queue + .extend(build_level_packets(&data.level).into_iter()); + + let username = player.username.clone(); + data.players.push(player); + + let spawn_packet = ServerPacket::SpawnPlayer { + player_id: *own_id, + player_name: username.clone(), + x: f16::from_f32(16.5), + y: f16::from_f32((data.level.y_size / 2 + 2) as f32), + z: f16::from_f32(16.5), + yaw: 0, + pitch: 0, + }; + let message_packet = ServerPacket::Message { + player_id: *own_id, + message: format!("&e{} has joined the server.", username), + }; + for player in &mut data.players { + player.packets_to_send.push(spawn_packet.clone()); + if player.id != *own_id { + reply_queue.push_back(ServerPacket::SpawnPlayer { + player_id: player.id, + player_name: player.username.clone(), + x: player.x, + y: player.y, + z: player.z, + yaw: player.yaw, + pitch: player.pitch, + }); + player.packets_to_send.push(message_packet.clone()); + } + } + reply_queue.push_back(ServerPacket::Message { + player_id: *own_id, + message: "Welcome to the server! Enjoyyyyyy".to_string(), + }); + reply_queue.push_back(ServerPacket::UpdateUserType { + user_type: PlayerType::Operator, + }); + } + ClientPacket::SetBlock { + x, + y, + z, + mode, + block_type, + } => { + let block_type = if mode == 0x00 { 0 } else { block_type }; + let mut data = data.write().await; + let block = + data.level.get_block(x as usize, y as usize, z as usize); + // check if bedrock + if block == 0x07 + && data + .players + .iter() + .find_map(|p| { + (p.id == *own_id).then_some(p.player_type) + }) + .unwrap_or_default() != PlayerType::Operator + { + continue; + } + let packet = ServerPacket::SetBlock { + x, + y, + z, + block_type, + }; + data.level + .set_block(x as usize, y as usize, z as usize, block_type); + for player in &mut data.players { + player.packets_to_send.push(packet.clone()); + } + } + ClientPacket::PositionOrientation { + _player_id: _, + x, + y, + z, + yaw, + pitch, + } => { + let packet = ServerPacket::SetPositionOrientation { + player_id: *own_id, + x, + y, + z, + yaw, + pitch, + }; + let mut data = data.write().await; + for player in &mut data.players { + player.packets_to_send.push(packet.clone()); + } + } + ClientPacket::Message { player_id, message } => { + let mut data = data.write().await; + println!("{message}"); + let message = format!( + "&f<{}> {message}", + data.players + .iter() + .find(|p| p.id == *own_id) + .expect("should never fail") + .username + ); + let packet = ServerPacket::Message { player_id, message }; + for player in &mut data.players { + player.packets_to_send.push(packet.clone()); + } + } + } + } else { + println!("unknown packet id: {:0x}", packet_buf[0]); + } + } + } + Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => continue, + Err(e) => return Err(e), + } + } + + if ready.is_writable() { + { + let mut data = data.write().await; + if let Some(player) = data.players.iter_mut().find(|p| p.id == *own_id) { + for mut packet in player.packets_to_send.drain(..) { + if let Some(id) = packet.get_player_id() { + if id == *own_id { + if !packet.should_echo() { + continue; + } + packet.set_player_id(-1); + } + } + reply_queue.push_back(packet); + } + } + } + + while let Some(packet) = reply_queue.pop_front() { + let writer = PacketWriter::default().write_u8(packet.get_id()); + let msg = packet.write(writer).into_raw_packet(); + stream.write_all(&msg).await?; + } + } + } + + println!("remaining packets: {}", reply_queue.len()); + + Ok(None) +} + +/// helper to put together packets that need to be sent to send full level data for the given level +fn build_level_packets(level: &Level) -> Vec { + let mut packets: Vec = vec![ServerPacket::LevelInitialize {}]; + + // TODO: the type conversions in here may be weird idk + let volume = level.x_size * level.y_size * level.z_size; + let mut data = Vec::with_capacity(volume + 4); + data.extend_from_slice(&(volume as i32).to_be_bytes()); + data.extend_from_slice(&level.blocks); + + let mut e = GzEncoder::new(Vec::new(), Compression::best()); + e.write_all(&data).expect("failed to gzip level data"); + let data = e.finish().expect("failed to gzip level data"); + let data_len = data.len(); + let mut total_bytes = 0; + + for chunk in data.chunks(ARRAY_LENGTH) { + let chunk_len = chunk.len(); + let percent_complete = (total_bytes * 100 / data_len) as u8; + packets.push(ServerPacket::LevelDataChunk { + chunk_length: chunk_len as i16, + chunk_data: chunk.to_vec(), + percent_complete, + }); + + total_bytes += chunk_len; + } + + packets.push(ServerPacket::LevelFinalize { + x_size: level.x_size as i16, + y_size: level.y_size as i16, + z_size: level.z_size as i16, + }); + + packets +}