From 9b1f9426369457ad1c7f03e98cc71213826fac63 Mon Sep 17 00:00:00 2001 From: TriMill Date: Mon, 24 Jul 2023 00:50:20 -0400 Subject: [PATCH] initial commit --- .gitignore | 1 + Cargo.lock | 580 ++++++++++++++++++++++++++++++ Cargo.toml | 15 + src/client.rs | 284 +++++++++++++++ src/event.rs | 15 + src/main.rs | 104 ++++++ src/protocol/common.rs | 60 ++++ src/protocol/mod.rs | 96 +++++ src/protocol/v1_19_4/mod.rs | 137 +++++++ src/protocol/v1_19_4/registry.nbt | Bin 0 -> 32769 bytes src/protocol/v1_20_1/mod.rs | 136 +++++++ src/protocol/v1_20_1/registry.nbt | Bin 0 -> 39164 bytes src/ser.rs | 244 +++++++++++++ src/varint.rs | 37 ++ 14 files changed, 1709 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 src/client.rs create mode 100644 src/event.rs create mode 100644 src/main.rs create mode 100644 src/protocol/common.rs create mode 100644 src/protocol/mod.rs create mode 100644 src/protocol/v1_19_4/mod.rs create mode 100644 src/protocol/v1_19_4/registry.nbt create mode 100644 src/protocol/v1_20_1/mod.rs create mode 100644 src/protocol/v1_20_1/registry.nbt create mode 100644 src/ser.rs create mode 100644 src/varint.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/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 0000000000000000000000000000000000000000..f4990fff7a9763c82c01e5295ebd4b2973828d90 GIT binary patch literal 32769 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, 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 0000000000000000000000000000000000000000..63e4a3a43cf51257bbbcd22d8ec6751b4b7cc3c9 GIT binary patch literal 39164 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 Yr^_sK27ZRmgwJ4aZxgHcx@;l-ANg5?)c^nh literal 0 HcmV?d00001 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 + } +}