commit 9b1f9426369457ad1c7f03e98cc71213826fac63 Author: TriMill Date: Mon Jul 24 00:50:20 2023 -0400 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..10895f8 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,580 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4fa78e18c64fce05e902adecd7a5eed15a5e0a3439f7b0e169f0252214865e3" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "aho-corasick" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" +dependencies = [ + "memchr", +] + +[[package]] +name = "anyhow" +version = "1.0.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b13c32d80ecc7ab747b80c3784bce54ee8a7a0cc4fbda9bf4cda2cf6fe90854" + +[[package]] +name = "async-trait" +version = "0.1.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a564d521dd56509c4c47480d00b80ee55f7e385ae48db5744c67ad50c92d2ebf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "backtrace" +version = "0.3.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4319208da049c43661739c5fade2ba182f09d1dc2299b32298d3a31692b17e12" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "bitflags" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "bytes" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" + +[[package]] +name = "cc" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "env_logger" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "errno" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" +dependencies = [ + "errno-dragonfly", + "libc", + "windows-sys", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "flate2" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "gimli" +version = "0.27.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e" + +[[package]] +name = "hematite-nbt" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "670d0784ee67cfb57393dc1837867d2951f9a59ca7db99a653499c854f745739" +dependencies = [ + "byteorder", + "cesu8", + "flate2", + "serde", +] + +[[package]] +name = "hermit-abi" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "is-terminal" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" +dependencies = [ + "hermit-abi", + "rustix", + "windows-sys", +] + +[[package]] +name = "itoa" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" + +[[package]] +name = "libc" +version = "0.2.147" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" + +[[package]] +name = "linux-raw-sys" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09fc20d2ca12cb9f044c93e3bd6d32d523e6e2ec3db4f7b2939cd99026ecd3f0" + +[[package]] +name = "log" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" +dependencies = [ + "libc", + "wasi", + "windows-sys", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + +[[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.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bda667d9f2b5051b8833f59f3bf748b28ef54f850f4fcb389a252aa383866d1" +dependencies = [ + "memchr", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c40d25201921e5ff0c862a505c6557ea88568a4e3ace775ab55e93f2f4f9d57" + +[[package]] +name = "proc-macro2" +version = "1.0.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quectocraft" +version = "0.2.0" +dependencies = [ + "anyhow", + "async-trait", + "env_logger", + "hematite-nbt", + "log", + "num-traits", + "serde_json", + "tokio", + "uuid", +] + +[[package]] +name = "quote" +version = "1.0.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fe8a65d69dd0808184ebb5f836ab526bb259db23c657efa38711b1072ee47f0" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2eae68fc220f7cf2532e4494aded17545fce192d59cd996e0fe7887f4ceb575" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39354c10dd07468c2e73926b23bb9c2caca74c5501e38a35da70406f1d923310" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2" + +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + +[[package]] +name = "rustix" +version = "0.38.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a962918ea88d644592894bc6dc55acc6c0956488adcebbfb6e273506b7fd6e5" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "ryu" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" + +[[package]] +name = "serde" +version = "1.0.171" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30e27d1e4fd7659406c492fd6cfaf2066ba8773de45ca75e855590f856dc34a9" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.171" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389894603bd18c46fa56231694f8d827779c0951a667087194cf9de94ed24682" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d03b412469450d4404fe8499a268edd7f8b79fecb074b0d812ad64ca21f4031b" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1_smol" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012" + +[[package]] +name = "socket2" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "syn" +version = "2.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45c3457aacde3c65315de5031ec191ce46604304d2446e803d71ade03308d970" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "termcolor" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "tokio" +version = "1.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "532826ff75199d5833b9d2c5fe410f29235e25704ee5f0ef599fb51c21f4a4da" +dependencies = [ + "autocfg", + "backtrace", + "bytes", + "libc", + "mio", + "num_cpus", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys", +] + +[[package]] +name = "tokio-macros" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" + +[[package]] +name = "uuid" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d" +dependencies = [ + "sha1_smol", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.48.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..c23d9ea --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "quectocraft" +version = "0.2.0" +edition = "2021" + +[dependencies] +tokio = { version = "1.29", features = ["macros", "rt-multi-thread", "io-util", "net", "sync", "time"] } +async-trait = "0.1" +num-traits = "0.2" +serde_json = "1.0" +anyhow = "1.0" +uuid = { version = "1.4", features = ["v5"] } +hematite-nbt = "0.5.2" +log = "0.4" +env_logger = "0.10" diff --git a/src/client.rs b/src/client.rs new file mode 100644 index 0000000..ee81d11 --- /dev/null +++ b/src/client.rs @@ -0,0 +1,284 @@ +use std::io::Cursor; + +use anyhow::anyhow; +use log::{info, debug}; +use serde_json::json; +use tokio::{net::{TcpStream, tcp::{OwnedReadHalf, OwnedWriteHalf}}, select, io::{AsyncReadExt, AsyncWriteExt}}; +use uuid::Uuid; + +use crate::{varint::VarInt, ser::{Deserializable, Position}, protocol::{self, Protocol, ProtocolState, ClientPacket, ServerPacket}, event::{ClientEvent, ServerEvent}, Player, ClientInfo}; + +type Sender = tokio::sync::mpsc::Sender<(u64, ClientEvent)>; +type Receiver = tokio::sync::mpsc::Receiver; + +const OFFLINE_NAMESPACE: Uuid = Uuid::from_bytes([0xc3, 0xe3, 0xe7, 0xa5, 0x58, 0xe5, 0x4c, 0xf4, 0x84, 0xef, 0x27, 0x4b, 0x9e, 0xf1, 0x5c, 0xc3]); + +fn offline_uuid(name: &str) -> Uuid { + Uuid::new_v5(&OFFLINE_NAMESPACE, name.as_bytes()) +} + +async fn read_varint_async(r: &mut OwnedReadHalf) -> std::io::Result> { + let mut buf = [0]; + let mut result = 0u32; + for count in 0..5 { + if r.read(&mut buf).await? != 1 { + return Ok(None) + } + let byte = buf[0]; + result |= ((byte & 0x7f) as u32) << (7 * count); + if byte & 0x80 == 0 { + return Ok(Some(result as i32)) + } + } + return Ok(None) +} + +async fn write_varint_async(w: &mut OwnedWriteHalf, i: i32) -> std::io::Result<()> { + let mut data = i as u32; + loop { + let mut byte = (data & 0x7F) as u8; + data >>= 7; + if data != 0 { + byte |= 0x80; + } + w.write_u8(byte).await?; + if data == 0 { + return Ok(()) + } + } +} + +struct ClientState { + id: u64, + proto: Protocol, + state: ProtocolState, + r: OwnedReadHalf, + w: OwnedWriteHalf, + buf: Vec +} + +enum ReadPacketOutcome { + Packet(ClientPacket), + None, + Eof, +} + +pub async fn run_client(id: u64, stream: TcpStream, tx: Sender, rx: Receiver) -> anyhow::Result<()> { + debug!("running client #{id}"); + let (r, w) = stream.into_split(); + let client = ClientState { + id, + proto: protocol::COMMON, + r, w, + buf: Vec::new(), + state: ProtocolState::Handshake, + }; + client.handshake(tx, rx).await +} + +impl ClientState { + async fn read_packet(&mut self) -> anyhow::Result { + match read_varint_async(&mut self.r).await? { + Some(len) => { + if len as usize > self.buf.len() { + self.buf.resize(len as usize, 0); + } + self.r.read_exact(&mut self.buf[0..(len as usize)]).await?; + let mut cursor = Cursor::new(&mut self.buf); + let id = VarInt::deserialize(&mut cursor)?.0; + let decoded = (self.proto.decode)(Box::new(&mut cursor), self.state, len, id)?; + Ok(match decoded { + Some(pk) => ReadPacketOutcome::Packet(pk), + None => ReadPacketOutcome::None, + }) + } + None => return Ok(ReadPacketOutcome::Eof), + } + } + + async fn write_packet(&mut self, pk: ServerPacket) -> anyhow::Result<()> { + self.buf.clear(); + let mut cursor = Cursor::new(&mut self.buf); + (self.proto.encode)(Box::new(&mut cursor), self.state, pk)?; + write_varint_async(&mut self.w, self.buf.len() as i32).await?; + self.w.write_all(&self.buf).await?; + Ok(()) + } + + + async fn handshake(mut self, tx: Sender, rx: Receiver) -> anyhow::Result<()> { + let ReadPacketOutcome::Packet(ClientPacket::Handshake { version, addr, port, next_state }) = self.read_packet().await? else { + return Err(anyhow!("Did not recieve handshake")) + }; + + self.state = ProtocolState::try_from(next_state).unwrap_or(ProtocolState::Handshake); + + if let Some(proto) = protocol::get_protocol(version) { + tx.send((self.id, ClientEvent::Handshake(ClientInfo { + addr, + port, + proto_version: proto.version, + proto_name: proto.name, + }))).await?; + self.proto = proto; + + info!("#{} using protocol {} ({})", self.id, proto.name, proto.version); + } else { + info!("#{} using unsupported protocol {}", self.id, version); + } + + match self.state { + ProtocolState::Status => self.slp(tx, rx).await, + ProtocolState::Login => self.login(tx, rx).await, + ProtocolState::Handshake + | ProtocolState::Play => Err(anyhow!("invalid next state")), + } + } + + async fn slp(mut self, _tx: Sender, mut rx: Receiver) -> anyhow::Result<()> { + debug!("#{} entering slp", self.id); + loop { select! { + ev = rx.recv() => match ev { + Some(ServerEvent::Disconnect(msg)) => { + debug!("#{} disconnecting: {}", self.id, msg.unwrap_or_default()); + return Ok(()) + }, + _ => (), + }, + pk = self.read_packet() => match pk? { + ReadPacketOutcome::Packet(ClientPacket::PingRequest(data)) => { + debug!("#{} ping request: {}", self.id, data); + self.write_packet(ServerPacket::PingResponse(data)).await?; + }, + ReadPacketOutcome::Packet(ClientPacket::StatusRequest) => { + debug!("#{} status request", self.id); + let desc = if self.proto == protocol::COMMON { + "Unsupported client version" + } else { + "server list ping" + }; + self.write_packet(ServerPacket::StatusResponse(json!( + { + "version": { + "name": self.proto.name, + "protocol": self.proto.version, + }, + "players": { + "max": 1337, + "online": 0, + }, + "description": { + "text": desc, + }, + "enforcesSecureChat": false, + "previewsChat": false, + } + ))).await?; + }, + ReadPacketOutcome::Eof => { + debug!("#{} got eof, closing", self.id); + return Ok(()) + }, + _ => (), + }, + } } + } + + async fn login(mut self, tx: Sender, mut rx: Receiver) -> anyhow::Result<()> { + debug!("#{} entering login", self.id); + if self.proto == protocol::COMMON { + self.write_packet(ServerPacket::LoginDisconnect(json!( + { + "text": "Unsupported client version" + } + ))).await?; + debug!("#{} disconnecting due to unsupported version", self.id); + return Ok(()) + } + loop { select! { + ev = rx.recv() => match ev { + Some(ServerEvent::Disconnect(msg)) => { + self.write_packet(ServerPacket::LoginDisconnect(json!( + { + "text": msg + } + ))).await?; + info!("#{} disconnecting: {}", self.id, msg.unwrap_or_default()); + return Ok(()) + }, + _ => (), + }, + pk = self.read_packet() => match pk? { + ReadPacketOutcome::Packet(ClientPacket::LoginStart { name, uuid }) => { + let uuid = uuid.unwrap_or_else(|| offline_uuid(&name)); + info!("#{} logged in as {name} ({uuid})", self.id); + self.write_packet(ServerPacket::LoginSuccess { name: name.clone(), uuid }).await?; + self.state = ProtocolState::Play; + tx.send((self.id, ClientEvent::Join(Player { name, uuid }))).await?; + return self.play(tx, rx).await + } + ReadPacketOutcome::Eof => { + debug!("#{} got eof, closing", self.id); + return Ok(()) + }, + _ => (), + }, + } } + } + + async fn play(mut self, tx: Sender, mut rx: Receiver) -> anyhow::Result<()> { + debug!("#{} entering play", self.id); + self.write_packet(ServerPacket::JoinGame { + eid: self.id as i32, + gamemode: 3, + hardcode: false, + }).await?; + + self.write_packet(ServerPacket::PluginMessage { + channel: "minecraft:brand".to_owned(), + data: b"\x0bquectocraft".to_vec(), + }).await?; + + for x in -1..=1 { + for z in -1..=1 { + self.write_packet(ServerPacket::ChunkData { x, z }).await?; + } + } + + self.write_packet(ServerPacket::SetDefaultSpawn { + pos: Position { x: 0, y: 0, z: 0 }, + angle: 0.0 + }).await?; + + loop { select! { + ev = rx.recv() => match ev { + Some(ServerEvent::Disconnect(msg)) => { + self.write_packet(ServerPacket::PlayDisconnect(json!( + { + "text": msg + } + ))).await?; + info!("#{} disconnecting: {}", self.id, msg.unwrap_or_default()); + return Ok(()) + }, + Some(ServerEvent::KeepAlive(data)) => { + self.write_packet(ServerPacket::KeepAlive(data)).await?; + debug!("#{} sending keepalive: {data}", self.id); + }, + None => (), + }, + pk = self.read_packet() => match pk? { + ReadPacketOutcome::Packet(pk) => match pk { + ClientPacket::KeepAlive(data) => { + debug!("#{} recieved keepalive: {data}", self.id); + tx.send((self.id, ClientEvent::KeepAlive(data))).await?; + }, + _ => (), + } + ReadPacketOutcome::None => (), + ReadPacketOutcome::Eof => return Ok(()), + }, + } } + } +} + diff --git a/src/event.rs b/src/event.rs new file mode 100644 index 0000000..e79c31b --- /dev/null +++ b/src/event.rs @@ -0,0 +1,15 @@ +use crate::{Player, ClientInfo}; + +#[derive(Debug)] +pub enum ClientEvent { + Handshake(ClientInfo), + Join(Player), + KeepAlive(i64), + Disconnect, +} + +#[derive(Debug)] +pub enum ServerEvent { + Disconnect(Option), + KeepAlive(i64), +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..743a66e --- /dev/null +++ b/src/main.rs @@ -0,0 +1,104 @@ +use std::{collections::HashMap, time::Duration, net::ToSocketAddrs}; + +use event::ServerEvent; +use log::{info, error}; +use tokio::{net::TcpListener, sync::mpsc::{Sender, self}, select}; +use uuid::Uuid; + +use crate::{client::run_client, event::ClientEvent}; + +mod event; +mod protocol; +mod ser; +mod varint; +mod client; + +pub type JsonValue = serde_json::Value; + +#[derive(Clone, Debug)] +pub struct ClientInfo { + addr: String, + port: u16, + proto_version: i32, + proto_name: &'static str, +} + +#[derive(Debug)] +pub struct Player { + name: String, + uuid: Uuid, +} + +struct Client { + tx: Sender, + keepalive: Option, + info: Option, + player: Option, +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + env_logger::init(); + let socket_addr = "0.0.0.0:25565".to_socket_addrs()?.next().expect("invalid server address"); + let listener = TcpListener::bind(socket_addr).await?; + let mut clients = HashMap::new(); + let mut next_id = 0; + let (c_tx, mut c_rx) = mpsc::channel(16); + let mut keepalive = tokio::time::interval(Duration::from_secs(15)); + info!("listening on {socket_addr}"); + loop { select! { + conn = listener.accept() => { + let c_tx = c_tx.clone(); + let (stream, addr) = conn?; + let id = next_id; + next_id += 1; + let (s_tx, s_rx) = mpsc::channel(16); + clients.insert(id, Client { + tx: s_tx, + keepalive: None, + info: None, + player: None, + }); + info!("#{id} connected from {addr}"); + tokio::spawn(async move { + let c_tx2 = c_tx.clone(); + match run_client(id, stream, c_tx, s_rx).await { + Ok(_) => info!("client {id} disconnected"), + Err(e) => error!("client {id} error: {e}"), + } + c_tx2.send((id, ClientEvent::Disconnect)).await.unwrap(); + }); + }, + _ = keepalive.tick() => { + for (_, client) in &mut clients { + if client.keepalive.is_some() { + client.tx.send(ServerEvent::Disconnect(Some("Failed to respond to keep alives".to_owned()))).await?; + } else if client.player.is_some() { + let data = 1; + client.keepalive = Some(data); + client.tx.send(ServerEvent::KeepAlive(data)).await?; + } + } + }, + ev = c_rx.recv() => { + let Some(ev) = ev else { + return Err("reciever closed".into()); + }; + let id = ev.0; + let client = clients.get_mut(&id).unwrap(); + match ev.1 { + ClientEvent::Handshake(info) => client.info = Some(info), + ClientEvent::Join(player) => clients.get_mut(&id).unwrap().player = Some(player), + ClientEvent::Disconnect => { clients.remove(&id); }, + ClientEvent::KeepAlive(data) => { + let client = clients.get_mut(&id).unwrap(); + if client.keepalive == Some(data) { + client.keepalive = None; + } else { + client.keepalive = Some(0); + } + } + } + } + } } +} diff --git a/src/protocol/common.rs b/src/protocol/common.rs new file mode 100644 index 0000000..0fddc62 --- /dev/null +++ b/src/protocol/common.rs @@ -0,0 +1,60 @@ +use std::io::{Write, Read}; + +use anyhow::anyhow; + +use super::{ServerPacket, ClientPacket}; +use crate::{varint::VarInt, ser::{Serializable, Deserializable}}; + +use super::{Protocol, ProtocolState}; + +pub const COMMON: Protocol = Protocol { + name: "common", + version: i32::MAX, + encode, + decode, +}; + +fn encode(mut w: Box<&mut dyn Write>, _state: ProtocolState, pk: ServerPacket) -> anyhow::Result<()> { + match pk { + ServerPacket::StatusResponse(response) => { + VarInt(0x00).serialize(&mut w)?; + response.serialize(&mut w)?; + }, + ServerPacket::PingResponse(payload) => { + VarInt(0x01).serialize(&mut w)?; + payload.serialize(&mut w)?; + }, + ServerPacket::LoginDisconnect(response) => { + VarInt(0x00).serialize(&mut w)?; + response.serialize(&mut w)?; + }, + _ => { + return Err(anyhow!("packet {pk:?} unhandled")) + } + } + Ok(()) +} + +fn decode(mut r: Box<&mut dyn Read>, state: ProtocolState, _len: i32, id: i32) -> anyhow::Result> { + type Ps = ProtocolState; + match (state, id) { + (Ps::Handshake, 0x00) => { + let version = VarInt::deserialize(&mut r)?.0; + let addr = String::deserialize(&mut r)?; + let port = u16::deserialize(&mut r)?; + let next_state = VarInt::deserialize(&mut r)?.0; + Ok(Some(ClientPacket::Handshake { version, addr, port, next_state })) + }, + (Ps::Status, 0x00) => { + Ok(Some(ClientPacket::StatusRequest)) + }, + (Ps::Status, 0x01) => { + let payload = i64::deserialize(&mut r)?; + Ok(Some(ClientPacket::PingRequest(payload))) + }, + _ => { + println!("invalid id {id:#x} in state {state:?}"); + Ok(None) + } + } +} diff --git a/src/protocol/mod.rs b/src/protocol/mod.rs new file mode 100644 index 0000000..798f8a5 --- /dev/null +++ b/src/protocol/mod.rs @@ -0,0 +1,96 @@ +use std::io::{Read, Write}; + +use uuid::Uuid; + +use crate::{JsonValue, ser::Position}; + +mod common; +mod v1_19_4; +mod v1_20_1; + +pub use common::COMMON as COMMON; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ProtocolState { + Handshake = 0, + Status = 1, + Login = 2, + Play = 3, +} + +impl TryFrom for ProtocolState { + type Error = (); + fn try_from(value: i32) -> std::result::Result { + match value { + 0 => Ok(Self::Handshake), + 1 => Ok(Self::Status), + 2 => Ok(Self::Login), + _ => Err(()) + } + } +} + +#[derive(Clone, Debug)] +pub enum ClientPacket { + Handshake { + version: i32, + addr: String, + port: u16, + next_state: i32 + }, + StatusRequest, + PingRequest(i64), + LoginStart { + name: String, + uuid: Option + }, + PluginMessage { + channel: String, + data: Vec + }, + KeepAlive(i64), +} + +#[derive(Clone, Debug)] +pub enum ServerPacket { + StatusResponse(JsonValue), + PingResponse(i64), + LoginDisconnect(JsonValue), + LoginSuccess { + name: String, + uuid: Uuid + }, + JoinGame { + eid: i32, + gamemode: u8, + hardcode: bool, + }, + PlayDisconnect(JsonValue), + KeepAlive(i64), + PluginMessage { + channel: String, + data: Vec + }, + ChunkData { + x: i32, + z: i32, + }, + SetDefaultSpawn { pos: Position, angle: f32 }, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Protocol { + pub name: &'static str, + pub version: i32, + pub encode: fn(w: Box<&mut dyn Write>, state: ProtocolState, ev: ServerPacket) -> anyhow::Result<()>, + pub decode: fn(r: Box<&mut dyn Read>, state: ProtocolState, len: i32, id: i32) -> anyhow::Result>, +} + +pub const fn get_protocol(version: i32) -> Option { + match version { + v1_20_1::VERSION => Some(v1_20_1::PROTOCOL), + v1_19_4::VERSION => Some(v1_19_4::PROTOCOL), + _ => None, + } +} + diff --git a/src/protocol/v1_19_4/mod.rs b/src/protocol/v1_19_4/mod.rs new file mode 100644 index 0000000..cd0f956 --- /dev/null +++ b/src/protocol/v1_19_4/mod.rs @@ -0,0 +1,137 @@ +use std::io::{Write, Read}; + +use uuid::Uuid; + +use crate::{ser::{Serializable, Deserializable}, varint::VarInt}; + +use super::{ServerPacket, ClientPacket, Protocol, ProtocolState, common::COMMON}; + +pub const VERSION: i32 = 762; + +pub const PROTOCOL: Protocol = Protocol { + name: "1.19.4", + version: VERSION, + encode, + decode, +}; + +fn encode(mut w: Box<&mut dyn Write>, state: ProtocolState, ev: ServerPacket) -> anyhow::Result<()> { + match ev { + ServerPacket::LoginSuccess { name, uuid } => { + VarInt(0x02).serialize(&mut w)?; + uuid.serialize(&mut w)?; + name.serialize(&mut w)?; + // number of properties (0) + VarInt(0x00).serialize(&mut w)?; + }, + ServerPacket::JoinGame { eid, gamemode, hardcode } => { + VarInt(0x28).serialize(&mut w)?; + eid.serialize(&mut w)?; + hardcode.serialize(&mut w)?; + gamemode.serialize(&mut w)?; + (-1i8).serialize(&mut w)?; // prev gamemode undefined + VarInt(1).serialize(&mut w)?; // dimension count + "qc:world".serialize(&mut w)?; // register one dimension + include_bytes!("registry.nbt").serialize(&mut w)?; // registry codec + "minecraft:the_end".serialize(&mut w)?; // dimension type + "qc:world".serialize(&mut w)?; // dimension name + 0i64.serialize(&mut w)?; // seed + VarInt(65535).serialize(&mut w)?; // max players + VarInt(8).serialize(&mut w)?; // view dist + VarInt(0).serialize(&mut w)?; // sim dist + false.serialize(&mut w)?; // reduce debug info + false.serialize(&mut w)?; // respawn screen + false.serialize(&mut w)?; // is debug + false.serialize(&mut w)?; // is flat + false.serialize(&mut w)?; // has death location + }, + ServerPacket::KeepAlive(data) => { + VarInt(0x23).serialize(&mut w)?; + data.serialize(&mut w)?; + }, + ServerPacket::PlayDisconnect(msg) => { + VarInt(0x1A).serialize(&mut w)?; + msg.serialize(&mut w)?; + }, + ServerPacket::PluginMessage { channel, mut data } => { + VarInt(0x17).serialize(&mut w)?; + channel.serialize(&mut w)?; + w.write_all(&mut data)?; + }, + ServerPacket::ChunkData { x, z } => { + VarInt(0x24).serialize(&mut w)?; + // chunk x + x.serialize(&mut w)?; + // chunk z + z.serialize(&mut w)?; + // heightmap + let hmdata = [0i64; 37]; + let mut heightmap = nbt::Blob::new(); + heightmap.insert("MOTION_BLOCKING", hmdata.as_ref()).unwrap(); + heightmap.serialize(&mut w)?; + // chunk data + let mut chunk_data: Vec = Vec::new(); + for _ in 0..(384 / 16) { + // number of non-air blocks + (0i16).serialize(&mut chunk_data)?; + // block states + (0u8).serialize(&mut chunk_data)?; + VarInt(0).serialize(&mut chunk_data)?; + VarInt(0).serialize(&mut chunk_data)?; + // biomes + (0u8).serialize(&mut chunk_data)?; + VarInt(0).serialize(&mut chunk_data)?; + VarInt(0).serialize(&mut chunk_data)?; + } + VarInt(chunk_data.len() as i32).serialize(&mut w)?; + w.write_all(&chunk_data)?; + // block entities + VarInt(0).serialize(&mut w)?; + // trust edges + true.serialize(&mut w)?; + // light masks + VarInt(0).serialize(&mut w)?; + VarInt(0).serialize(&mut w)?; + VarInt(0).serialize(&mut w)?; + VarInt(0).serialize(&mut w)?; + // sky light array len + VarInt(0).serialize(&mut w)?; + // block light array len + VarInt(0).serialize(&mut w)?; + }, + ServerPacket::SetDefaultSpawn { pos, angle } => { + VarInt(0x50).serialize(&mut w)?; + pos.serialize(&mut w)?; + angle.serialize(&mut w)?; + }, + _ => { (COMMON.encode)(w, state, ev)?; } + } + Ok(()) +} + +fn decode(mut r: Box<&mut dyn Read>, state: ProtocolState, len: i32, id: i32) -> anyhow::Result> { + type Ps = ProtocolState; + match (state, id) { + (Ps::Login, 0x00) => { + let name = String::deserialize(&mut r)?; + let has_uuid = bool::deserialize(&mut r)?; + let uuid = if has_uuid { + Some(Uuid::deserialize(&mut r)?) + } else { None }; + Ok(Some(ClientPacket::LoginStart { name, uuid })) + } + (Ps::Login, 0x01 | 0x02) => Ok(None), // unsupported + (Ps::Play, 0x0D) => { + let channel = String::deserialize(&mut r)?; + let mut data = Vec::new(); + r.read_to_end(&mut data)?; + Ok(Some(ClientPacket::PluginMessage { channel, data })) + } + (Ps::Play, 0x12) => { + let data = i64::deserialize(&mut r)?; + Ok(Some(ClientPacket::KeepAlive(data))) + } + (Ps::Play, 0x14 | 0x15 | 0x16) => Ok(None), // position & rotation + _ => (COMMON.decode)(r, state, len, id) + } +} diff --git a/src/protocol/v1_19_4/registry.nbt b/src/protocol/v1_19_4/registry.nbt new file mode 100644 index 0000000..f4990ff Binary files /dev/null and b/src/protocol/v1_19_4/registry.nbt differ diff --git a/src/protocol/v1_20_1/mod.rs b/src/protocol/v1_20_1/mod.rs new file mode 100644 index 0000000..7ea1186 --- /dev/null +++ b/src/protocol/v1_20_1/mod.rs @@ -0,0 +1,136 @@ +use std::io::{Write, Read}; + +use uuid::Uuid; + +use crate::{ser::{Serializable, Deserializable}, varint::VarInt}; + +use super::{ServerPacket, ClientPacket, Protocol, ProtocolState, common::COMMON}; + +pub const VERSION: i32 = 763; + +pub const PROTOCOL: Protocol = Protocol { + name: "1.20.1", + version: VERSION, + encode, + decode, +}; + +fn encode(mut w: Box<&mut dyn Write>, state: ProtocolState, ev: ServerPacket) -> anyhow::Result<()> { + match ev { + ServerPacket::LoginSuccess { name, uuid } => { + VarInt(0x02).serialize(&mut w)?; + uuid.serialize(&mut w)?; + name.serialize(&mut w)?; + // number of properties (0) + VarInt(0x00).serialize(&mut w)?; + }, + ServerPacket::JoinGame { eid, gamemode, hardcode } => { + VarInt(0x28).serialize(&mut w)?; + eid.serialize(&mut w)?; + hardcode.serialize(&mut w)?; + gamemode.serialize(&mut w)?; + (-1i8).serialize(&mut w)?; // prev gamemode undefined + VarInt(1).serialize(&mut w)?; // dimension count + "qc:world".serialize(&mut w)?; // register one dimension + include_bytes!("registry.nbt").serialize(&mut w)?; // registry codec + "minecraft:the_end".serialize(&mut w)?; // dimension type + "qc:world".serialize(&mut w)?; // dimension name + 0i64.serialize(&mut w)?; // seed + VarInt(65535).serialize(&mut w)?; // max players + VarInt(8).serialize(&mut w)?; // view dist + VarInt(0).serialize(&mut w)?; // sim dist + false.serialize(&mut w)?; // reduce debug info + false.serialize(&mut w)?; // respawn screen + false.serialize(&mut w)?; // is debug + false.serialize(&mut w)?; // is flat + false.serialize(&mut w)?; // has death location + VarInt(1).serialize(&mut w)?; // portal cooldown + }, + ServerPacket::KeepAlive(data) => { + VarInt(0x23).serialize(&mut w)?; + data.serialize(&mut w)?; + }, + ServerPacket::PlayDisconnect(msg) => { + VarInt(0x1A).serialize(&mut w)?; + msg.serialize(&mut w)?; + }, + ServerPacket::PluginMessage { channel, mut data } => { + VarInt(0x17).serialize(&mut w)?; + channel.serialize(&mut w)?; + w.write_all(&mut data)?; + }, + ServerPacket::ChunkData { x, z } => { + VarInt(0x24).serialize(&mut w)?; + // chunk x + x.serialize(&mut w)?; + // chunk z + z.serialize(&mut w)?; + // heightmap + let hmdata = [0i64; 37]; + let mut heightmap = nbt::Blob::new(); + heightmap.insert("MOTION_BLOCKING", hmdata.as_ref()).unwrap(); + heightmap.serialize(&mut w)?; + // chunk data + let mut chunk_data: Vec = Vec::new(); + for _ in 0..(384 / 16) { + // number of non-air blocks + (0i16).serialize(&mut chunk_data)?; + // block states + (0u8).serialize(&mut chunk_data)?; + VarInt(0).serialize(&mut chunk_data)?; + VarInt(0).serialize(&mut chunk_data)?; + // biomes + (0u8).serialize(&mut chunk_data)?; + VarInt(0).serialize(&mut chunk_data)?; + VarInt(0).serialize(&mut chunk_data)?; + } + VarInt(chunk_data.len() as i32).serialize(&mut w)?; + w.write_all(&chunk_data)?; + // block entities + VarInt(0).serialize(&mut w)?; + // light masks + VarInt(0).serialize(&mut w)?; + VarInt(0).serialize(&mut w)?; + VarInt(0).serialize(&mut w)?; + VarInt(0).serialize(&mut w)?; + // sky light array len + VarInt(0).serialize(&mut w)?; + // block light array len + VarInt(0).serialize(&mut w)?; + }, + ServerPacket::SetDefaultSpawn { pos, angle } => { + VarInt(0x50).serialize(&mut w)?; + pos.serialize(&mut w)?; + angle.serialize(&mut w)?; + }, + _ => { (COMMON.encode)(w, state, ev)?; } + } + Ok(()) +} + +fn decode(mut r: Box<&mut dyn Read>, state: ProtocolState, len: i32, id: i32) -> anyhow::Result> { + type Ps = ProtocolState; + match (state, id) { + (Ps::Login, 0x00) => { + let name = String::deserialize(&mut r)?; + let has_uuid = bool::deserialize(&mut r)?; + let uuid = if has_uuid { + Some(Uuid::deserialize(&mut r)?) + } else { None }; + Ok(Some(ClientPacket::LoginStart { name, uuid })) + } + (Ps::Login, 0x01 | 0x02) => Ok(None), // unsupported + (Ps::Play, 0x0D) => { + let channel = String::deserialize(&mut r)?; + let mut data = Vec::new(); + r.read_to_end(&mut data)?; + Ok(Some(ClientPacket::PluginMessage { channel, data })) + } + (Ps::Play, 0x12) => { + let data = i64::deserialize(&mut r)?; + Ok(Some(ClientPacket::KeepAlive(data))) + } + (Ps::Play, 0x14 | 0x15 | 0x16) => Ok(None), // position & rotation + _ => (COMMON.decode)(r, state, len, id) + } +} diff --git a/src/protocol/v1_20_1/registry.nbt b/src/protocol/v1_20_1/registry.nbt new file mode 100644 index 0000000..63e4a3a Binary files /dev/null and b/src/protocol/v1_20_1/registry.nbt differ diff --git a/src/ser.rs b/src/ser.rs new file mode 100644 index 0000000..dcda2a7 --- /dev/null +++ b/src/ser.rs @@ -0,0 +1,244 @@ +use std::{io::{self, Read, Write}, mem::MaybeUninit}; + +use anyhow::anyhow; +use uuid::Uuid; + +use crate::JsonValue; + +use super::varint::{VarInt, VarLong}; + +pub trait ReadExt: Read { + fn readb(&mut self) -> io::Result { + Ok(self.readn::<1>()?[0]) + } + + fn readn(&mut self) -> io::Result<[u8; N]> { + let mut buf = [0; N]; + self.read_exact(&mut buf)?; + Ok(buf) + } +} + +impl ReadExt for T {} + +pub trait Serializable: Sized { + fn serialize(&self, w: &mut impl Write) -> anyhow::Result<()>; +} + +pub trait Deserializable: Sized { + fn deserialize(r: &mut impl ReadExt) -> anyhow::Result; +} + +impl Serializable for bool { + fn serialize(&self, w: &mut impl Write) -> anyhow::Result<()> { + w.write_all(&[*self as u8])?; + Ok(()) + } +} + +impl Deserializable for bool { + fn deserialize(r: &mut impl ReadExt) -> anyhow::Result { + match r.readb()? { + 0 => Ok(false), + 1 => Ok(true), + b => Err(anyhow!("value {b} out of range for bool")) + } + } +} + +macro_rules! serialize_primitive { + ($ty:ty) => { + impl Serializable for $ty { + fn serialize(&self, w: &mut impl Write) -> anyhow::Result<()> { + w.write_all(&self.to_be_bytes())?; + Ok(()) + } + } + + impl Deserializable for $ty { + fn deserialize(r: &mut impl ReadExt) -> anyhow::Result { + Ok(Self::from_be_bytes(r.readn()?)) + } + } + }; +} + +serialize_primitive!(i8); +serialize_primitive!(i16); +serialize_primitive!(i32); +serialize_primitive!(i64); +serialize_primitive!(i128); +serialize_primitive!(u8); +serialize_primitive!(u16); +serialize_primitive!(u32); +serialize_primitive!(u64); +serialize_primitive!(u128); +serialize_primitive!(f32); +serialize_primitive!(f64); + +impl Serializable for VarInt { + fn serialize(&self, w: &mut impl Write) -> anyhow::Result<()> { + let mut data = self.0 as u32; + loop { + let mut byte = (data & 0x7F) as u8; + data >>= 7; + if data != 0 { + byte |= 0x80; + } + w.write_all(&[byte])?; + if data == 0 { + return Ok(()) + } + } + } +} + +impl Deserializable for VarInt { + fn deserialize(r: &mut impl ReadExt) -> anyhow::Result { + let mut result = 0u32; + for count in 0..5 { + let byte = r.readb()?; + result |= ((byte & 0x7f) as u32) << (7 * count); + if byte & 0x80 == 0 { + return Ok(VarInt(result as i32)) + } + } + return Err(anyhow!("VarInt too long")) + } +} + +impl Serializable for VarLong { + fn serialize(&self, w: &mut impl Write) -> anyhow::Result<()> { + let mut data = self.0 as u64; + loop { + let mut byte = (data & 0x7F) as u8; + data >>= 7; + if data != 0 { + byte |= 0x80; + } + w.write_all(&[byte])?; + if data == 0 { + return Ok(()) + } + } + } +} + +impl Deserializable for VarLong { + fn deserialize(r: &mut impl ReadExt) -> anyhow::Result { + let mut result = 0u64; + for count in 0..10 { + let byte = r.readb()?; + result |= ((byte & 0x7f) as u64) << (7 * count); + if byte & 0x80 == 0 { + return Ok(VarLong(result as i64)) + } + } + return Err(anyhow!("VarLong too long")) + } +} + +impl Serializable for String { + fn serialize(&self, w: &mut impl Write) -> anyhow::Result<()> { + VarInt(self.len() as i32).serialize(w)?; + w.write_all(self.as_bytes())?; + Ok(()) + } +} + +impl Deserializable for String { + fn deserialize(r: &mut impl ReadExt) -> anyhow::Result { + let len = VarInt::deserialize(r)?.0 as u64; + let mut buf = String::new(); + r.take(len).read_to_string(&mut buf)?; + Ok(buf) + } +} + +impl Serializable for &str { + fn serialize(&self, w: &mut impl Write) -> anyhow::Result<()> { + VarInt(self.len() as i32).serialize(w)?; + w.write_all(self.as_bytes())?; + Ok(()) + } +} + +impl Serializable for JsonValue { + fn serialize(&self, w: &mut impl Write) -> anyhow::Result<()> { + serde_json::to_string(self)?.serialize(w) + } +} + +impl Deserializable for JsonValue { + fn deserialize(r: &mut impl ReadExt) -> anyhow::Result { + let v = serde_json::from_str(&String::deserialize(r)?)?; + Ok(v) + } +} + +impl Serializable for [T; N] +where T: Serializable { + fn serialize(&self, w: &mut impl Write) -> anyhow::Result<()> { + for item in self { + item.serialize(w)?; + } + Ok(()) + } +} +impl Deserializable for [T; N] +where T: Deserializable { + fn deserialize(r: &mut impl ReadExt) -> anyhow::Result { + let mut arr: [MaybeUninit; N] = unsafe { + MaybeUninit::uninit().assume_init() + }; + for i in 0..N { + arr[i] = MaybeUninit::new(T::deserialize(r)?); + } + // Safety: since MaybeUninit and T have the same memory + // layout, so will [MaybeUninit; N] and [T; N]. + // std::mem::transmute cannot yet handle arrays of + // generic size. + Ok(unsafe { arr.as_ptr().cast::().read() }) + } +} + +impl Serializable for Uuid { + fn serialize(&self, w: &mut impl Write) -> anyhow::Result<()> { + self.as_u128().serialize(w) + } +} + +impl Deserializable for Uuid { + fn deserialize(r: &mut impl ReadExt) -> anyhow::Result { + Ok(Uuid::from_u128(u128::deserialize(r)?)) + } +} + +impl Serializable for nbt::Blob { + fn serialize(&self, w: &mut impl Write) -> anyhow::Result<()> { + self.to_writer(w)?; + Ok(()) + } +} + +impl Deserializable for nbt::Blob { + fn deserialize(r: &mut impl ReadExt) -> anyhow::Result { + Ok(nbt::Blob::from_reader(r)?) + } +} + +#[derive(Clone, Copy, Debug)] +pub struct Position { + pub x: i32, + pub y: i16, + pub z: i32, +} + +impl Serializable for Position { + fn serialize(&self, w: &mut impl Write) -> anyhow::Result<()> { + let value = (((self.x & 0x3FFFFFF) as u64) << 38) | + (((self.z & 0x3FFFFFF) as u64) << 12) | + (((self.y & 0xFFF) as u64) << 38); + value.serialize(w) + } +} diff --git a/src/varint.rs b/src/varint.rs new file mode 100644 index 0000000..356dc03 --- /dev/null +++ b/src/varint.rs @@ -0,0 +1,37 @@ +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub struct VarInt(pub i32); + +impl AsRef for VarInt { + fn as_ref(&self) -> &i32 { &self.0 } +} + +impl From for VarInt { + fn from(value: i32) -> Self { + Self(value) + } +} + +impl From for i32 { + fn from(value: VarInt) -> Self { + value.0 + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub struct VarLong(pub i64); + +impl AsRef for VarLong { + fn as_ref(&self) -> &i64 { &self.0 } +} + +impl From for VarLong { + fn from(value: i64) -> Self { + Self(value) + } +} + +impl From for i64 { + fn from(value: VarLong) -> Self { + value.0 + } +}