Compare commits

...

10 Commits

Author SHA1 Message Date
TriMill beac4bdb42 update things 2023-01-01 15:28:35 -05:00
TriMill 459245bf8b plugin documentation 2022-12-18 13:19:54 -05:00
TriMill 12677b2068 clippy 2022-12-18 00:50:37 -05:00
TriMill 278e28ad18 refactored 2022-12-18 00:43:01 -05:00
TriMill a3375c53a4 add logging and serde rename things 2022-12-17 15:45:43 -05:00
TriMill d4cf718214 added velocity support 2022-12-17 15:39:56 -05:00
TriMill 8434884747 works on 1.19.3 now 2022-12-17 00:48:05 -05:00
TriMill 0f067d6114 changes 2022-12-17 00:10:01 -05:00
TriMill 162d337769 more things 2022-12-05 11:31:45 -05:00
TriMill e1ccb6ec7d improvements 2022-12-04 01:06:21 -05:00
24 changed files with 2041 additions and 509 deletions

1
.gitignore vendored
View File

@ -1 +1,2 @@
/target
config.json

618
Cargo.lock generated
View File

@ -8,12 +8,45 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "aho-corasick"
version = "0.7.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac"
dependencies = [
"memchr",
]
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[package]]
name = "autocfg"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "block-buffer"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e"
dependencies = [
"generic-array",
]
[[package]]
name = "bstr"
version = "0.2.17"
@ -23,6 +56,12 @@ dependencies = [
"memchr",
]
[[package]]
name = "bumpalo"
version = "3.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba"
[[package]]
name = "byteorder"
version = "1.4.3"
@ -47,6 +86,46 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chrono"
version = "0.4.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f"
dependencies = [
"iana-time-zone",
"js-sys",
"num-integer",
"num-traits",
"time",
"wasm-bindgen",
"winapi",
]
[[package]]
name = "codespan-reporting"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e"
dependencies = [
"termcolor",
"unicode-width",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
[[package]]
name = "cpufeatures"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320"
dependencies = [
"libc",
]
[[package]]
name = "crc32fast"
version = "1.3.2"
@ -56,6 +135,120 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "crypto-common"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
dependencies = [
"generic-array",
"typenum",
]
[[package]]
name = "cxx"
version = "1.0.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bdf07d07d6531bfcdbe9b8b739b104610c6508dcc4d63b410585faf338241daf"
dependencies = [
"cc",
"cxxbridge-flags",
"cxxbridge-macro",
"link-cplusplus",
]
[[package]]
name = "cxx-build"
version = "1.0.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2eb5b96ecdc99f72657332953d4d9c50135af1bac34277801cc3937906ebd39"
dependencies = [
"cc",
"codespan-reporting",
"once_cell",
"proc-macro2",
"quote",
"scratch",
"syn",
]
[[package]]
name = "cxxbridge-flags"
version = "1.0.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac040a39517fd1674e0f32177648334b0f4074625b5588a64519804ba0553b12"
[[package]]
name = "cxxbridge-macro"
version = "1.0.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1362b0ddcfc4eb0a1f57b68bd77dd99f0e826958a96abd0ae9bd092e114ffed6"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "digest"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f"
dependencies = [
"block-buffer",
"crypto-common",
"subtle",
]
[[package]]
name = "either"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797"
[[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 = "erased-serde"
version = "0.3.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54558e0ba96fbe24280072642eceb9d7d442e32c7ec0ea9e7ecd7b4ea2cf4e11"
dependencies = [
"serde",
]
[[package]]
name = "errno"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1"
dependencies = [
"errno-dragonfly",
"libc",
"winapi",
]
[[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.25"
@ -66,6 +259,16 @@ dependencies = [
"miniz_oxide",
]
[[package]]
name = "generic-array"
version = "0.14.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9"
dependencies = [
"typenum",
"version_check",
]
[[package]]
name = "hematite-nbt"
version = "0.5.2"
@ -78,12 +281,130 @@ dependencies = [
"serde",
]
[[package]]
name = "hermit-abi"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7"
dependencies = [
"libc",
]
[[package]]
name = "hmac"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
dependencies = [
"digest",
]
[[package]]
name = "humantime"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
[[package]]
name = "iana-time-zone"
version = "0.1.53"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"wasm-bindgen",
"winapi",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca"
dependencies = [
"cxx",
"cxx-build",
]
[[package]]
name = "io-lifetimes"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46112a93252b123d31a119a8d1a1ac19deac4fac6e0e8b0df58f0d4e5870e63c"
dependencies = [
"libc",
"windows-sys",
]
[[package]]
name = "is-terminal"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "927609f78c2913a6f6ac3c27a4fe87f43e2a35367c0c4b0f8265e8f49a104330"
dependencies = [
"hermit-abi",
"io-lifetimes",
"rustix",
"windows-sys",
]
[[package]]
name = "itertools"
version = "0.10.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4217ad341ebadf8d8e724e264f13e593e0648f5b3e94b3896a5df283be015ecc"
[[package]]
name = "js-sys"
version = "0.3.60"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47"
dependencies = [
"wasm-bindgen",
]
[[package]]
name = "libc"
version = "0.2.138"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db6d7e329c562c5dfab7a46a2afabc8b987ab9a4834c9d1ca04dc54c1546cef8"
[[package]]
name = "link-cplusplus"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9272ab7b96c9046fbc5bc56c06c117cb639fe2d509df0c421cad82d2915cf369"
dependencies = [
"cc",
]
[[package]]
name = "linux-raw-sys"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f9f08d8963a6c613f4b1a78f4f4a4dbfadf8e6545b2d72861731e4858b8b47f"
[[package]]
name = "log"
version = "0.4.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
dependencies = [
"cfg-if",
]
[[package]]
name = "memchr"
version = "2.5.0"
@ -107,10 +428,38 @@ checksum = "4351dbcc863fb6249c81b3bd0c8001214e9d4d44d22cabda17026353a77fe612"
dependencies = [
"bstr",
"cc",
"erased-serde",
"mlua_derive",
"num-traits",
"once_cell",
"pkg-config",
"rustc-hash",
"serde",
]
[[package]]
name = "mlua_derive"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9214e60d3cf1643013b107330fcd374ccec1e4ba1eef76e7e5da5e8202e71c0"
dependencies = [
"itertools",
"once_cell",
"proc-macro-error",
"proc-macro2",
"quote",
"regex",
"syn",
]
[[package]]
name = "num-integer"
version = "0.1.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9"
dependencies = [
"autocfg",
"num-traits",
]
[[package]]
@ -134,6 +483,30 @@ version = "0.3.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160"
[[package]]
name = "proc-macro-error"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
dependencies = [
"proc-macro-error-attr",
"proc-macro2",
"quote",
"syn",
"version_check",
]
[[package]]
name = "proc-macro-error-attr"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
dependencies = [
"proc-macro2",
"quote",
"version_check",
]
[[package]]
name = "proc-macro2"
version = "1.0.47"
@ -147,10 +520,15 @@ dependencies = [
name = "quectocraft"
version = "0.1.0"
dependencies = [
"chrono",
"env_logger",
"hematite-nbt",
"hmac",
"log",
"mlua",
"serde",
"serde_json",
"sha2",
"uuid",
]
@ -163,18 +541,55 @@ dependencies = [
"proc-macro2",
]
[[package]]
name = "regex"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e076559ef8e241f2ae3479e36f97bd5741c0330689e217ad51ce2c76808b868a"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.6.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848"
[[package]]
name = "rustc-hash"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]]
name = "rustix"
version = "0.36.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb93e85278e08bb5788653183213d3a60fc242b10cb9be96586f5a73dcb67c23"
dependencies = [
"bitflags",
"errno",
"io-lifetimes",
"libc",
"linux-raw-sys",
"windows-sys",
]
[[package]]
name = "ryu"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09"
[[package]]
name = "scratch"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8132065adcfd6e02db789d9285a0deb2f3fcb04002865ab67d5fb103533898"
[[package]]
name = "serde"
version = "1.0.148"
@ -206,6 +621,23 @@ dependencies = [
"serde",
]
[[package]]
name = "sha2"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]]
name = "subtle"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
[[package]]
name = "syn"
version = "1.0.105"
@ -217,14 +649,200 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "termcolor"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755"
dependencies = [
"winapi-util",
]
[[package]]
name = "time"
version = "0.1.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a"
dependencies = [
"libc",
"wasi",
"winapi",
]
[[package]]
name = "typenum"
version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba"
[[package]]
name = "unicode-ident"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3"
[[package]]
name = "unicode-width"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
[[package]]
name = "uuid"
version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "422ee0de9031b5b948b97a8fc04e3aa35230001a722ddd27943e0be31564ce4c"
[[package]]
name = "version_check"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "wasi"
version = "0.10.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
[[package]]
name = "wasm-bindgen"
version = "0.2.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268"
dependencies = [
"cfg-if",
"wasm-bindgen-macro",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142"
dependencies = [
"bumpalo",
"log",
"once_cell",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c"
dependencies = [
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f"
[[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.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7"
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.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e"
[[package]]
name = "windows_aarch64_msvc"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4"
[[package]]
name = "windows_i686_gnu"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7"
[[package]]
name = "windows_i686_msvc"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246"
[[package]]
name = "windows_x86_64_gnu"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028"
[[package]]
name = "windows_x86_64_msvc"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5"

View File

@ -9,5 +9,10 @@ edition = "2021"
hematite-nbt = "0.5"
serde_json = "1.0"
serde = "1.0"
mlua = { version = "0.8", features = ["lua54"] }
mlua = { version = "0.8", features = ["lua54", "macros", "serialize"] }
uuid = "1.2"
log = "0.4.0"
env_logger = "0.10.0"
chrono = "0.4"
hmac = "0.12"
sha2 = "0.10"

13
README.md Normal file
View File

@ -0,0 +1,13 @@
# Quectocraft
Quectocraft is a minimal, extensible, efficient Minecraft server implementation written in Rust and Lua.
## Goals
- Minimal: By default, Quectocraft does very little by itself. It accepts connections, encodes and decodes packets, and handles the login sequence for you, but everything else must be done via plugins.
- Extensible: Via its Lua plugin system, Quectocraft can be configured to do a variety of things.
- Efficient: The vanilla Minecraft server, and even more efficient servers like Spigot and Paper, all use significant amounts of CPU even while idling with no players connected. Due to its low CPU and memory usage, Quectocraft is suitable for running on lower-end systems, or alongside another server without causing additional lag.
## Plugin API
See [Plugin API](docs/plugins.md)

50
docs/plugins.md Normal file
View File

@ -0,0 +1,50 @@
# Quectocraft Plugin API
Quectocraft plugins are written in Lua. Examples can be seen in the [plugins directory](plugins). Plugins can either be a single Lua file or a directory containing a file named `main.lua`.
## Plugin table
All information about a plugin is stored in a table, which must be returned at the end of the plugin. This table contains both information about the plugin and functions that act as event handlers.
| Field | Description |
|--------------------|--------------------------------------------------------------------------------------------------------------------------------------------------|
| `id` | The plugin's ID. This should consist of lowercase letters and underscores only. |
| `name` | The plugin's human-readable name. |
| `description` | The plugin's description. |
| `authors` | A list of the plugin's authors. |
| `version` | The plugin's version (semantic versioning encouraged). |
| `init` | Called when the plugin is initialized. The `server` table is available at this point. |
| `registerCommands` | Called to register the plugin's commands. Arguments: a `registry` table. |
| `playerJoin` | Called when a player joins. Arguments: the player's name, the player's UUID. |
| `playerLeave` | Called when a player leaves. Arguments: the player's name, the player's UUID. |
| `chatMessage` | Called when a player sends a chat message. Arguments: the message, the player's name and UUID. |
| `pluginMessage` | Called when a client sends a [plugin message](https://wiki.vg/Plugin_channels). Arguments: the channel, the message, the player's name and UUID. |
| `command` | Called when a player runs a command. Arguments: the command, the arguments, the player's name and UUID. |
## The `server` table
The `server` table is used to interact with the server. It has the following fields:
| Field | Description |
|---------------------|------------------------------------------------------------------------------------------------------------------------------------|
| `players` | A map from UUIDs to player names. |
| `sendPluginMessage` | Send a player a [plugin message](https://wiki.vg/Plugin_channels). Arguments: the player (name or UUID), the channel, the message. |
| `sendMessage` | Send a player a message. Arguments: the player (name or UUID), the message. |
| `broadcast` | Broadcast a message to all online players. Arguments: the message. |
| `disconnect` | Disconnect a player from the server. Arguments: the player (name or UUID), the reason (optional) |
## The `registry` table
The `registry` table is used to register commands. It is only available from the `registerCommands` event handler.
| Field | Description |
|--------------|---------------------------------------------------|
| `addCommand` | Add a command. Arguments: the name of the command |
## The `logger` table
The `logger` table is used to log information the the console. It has the following functions for different logging levels: `trace`, `debug`, `info`, `error`, `warn`. A logger should be initialized in the `init` event handler.
## Chat components
Wherever a chat component is expected (chat messages, disconnect reasons), the plugin can either provide a string or a chat component. Lua tables are a very good approximation for JSON, and as such translating between JSON chat components and tables is not very difficult. See [the wiki.vg documentation for chat components](https://wiki.vg/Chat) for more information.

View File

@ -1,23 +0,0 @@
local plugin = {
id = "example_plugin",
name = "Example Plugin",
version = "0.1.0",
}
function plugin.init()
print("PLUGIN init")
end
function plugin.playerJoin(name, uuid)
print("PLUGIN player joined: " .. name .. " uuid " .. uuid)
end
function plugin.playerLeave(name, uuid)
print("PLUGIN player left: " .. name .. " uuid " .. uuid)
end
function plugin.chatMessage(message, author, authorUuid)
print("PLUGIN message from " .. author .. ": " .. message)
end
return plugin

45
plugins/mcchat/main.lua Normal file
View File

@ -0,0 +1,45 @@
local plugin = {
id = "mcchat",
name = "MCChat",
description = "Provides Minecraft-style chat. Messages sent by one client will be broadcasted to every client.",
authors = { "trimill" },
version = "0.1.0",
}
local logger = nil
function plugin.init()
logger = server.initLogger(plugin)
logger.info("MCChat version " .. plugin.version)
end
function plugin.playerJoin(name)
logger.info(name .. " joined the game")
server.broadcast({
translate = "multiplayer.player.joined",
with = { {text = name} },
color = "yellow"
})
end
function plugin.playerLeave(name)
logger.info(name .. " left the game")
server.broadcast({
translate = "multiplayer.player.left",
with = { {text = name} },
color = "yellow"
})
end
function plugin.chatMessage(message, author)
logger.info("<" .. author .. "> " .. message)
server.broadcast({
translate = "chat.type.text",
with = {
{text = author},
{text = message}
}
})
end
return plugin

27
plugins/testcmd.lua Normal file
View File

@ -0,0 +1,27 @@
local plugin = {
id = "testcmd",
name = "TestCmd",
description = "eufdahjklfhjakl",
authors = { "trimill" },
version = "0.1.0",
}
local logger = nil
function plugin.init()
logger = server.initLogger(plugin)
end
function plugin.registerCommands(registry)
registry.addCommand("test")
end
function plugin.command(command, args, name, uuid)
logger.info("player " .. name .. " ran /" .. command .. " " .. args)
end
function plugin.playerJoin(name, uuid)
logger.info("player joined: " .. name .. " with uuid " .. uuid)
end
return plugin

27
src/config.rs Normal file
View File

@ -0,0 +1,27 @@
use std::{net::IpAddr, fs::OpenOptions};
use serde::Deserialize;
#[derive(Deserialize, PartialEq, Eq)]
#[serde(rename_all="lowercase")]
pub enum LoginMode {
Offline,
Velocity,
// TODO online, bungeecord
}
#[derive(Deserialize)]
pub struct Config {
pub addr: IpAddr,
pub port: u16,
pub login: LoginMode,
pub velocity_secret: Option<String>,
}
pub fn load_config() -> Result<Config, Box<dyn std::error::Error>> {
let config: Config = serde_json::from_reader(OpenOptions::new().read(true).open("./config.json")?)?;
if config.login == LoginMode::Velocity && config.velocity_secret.is_none() {
Err("Velocity is enabled but no secret is configured")?
}
Ok(config)
}

View File

@ -1,30 +1,69 @@
use std::borrow::Cow;
use std::time::Duration;
use std::io::Write;
use chrono::Utc;
use env_logger::Env;
use log::{info, warn};
use mlua::Lua;
use network::NetworkServer;
use plugins::{Plugin, Plugins};
use plugins::Plugins;
use crate::config::{load_config, LoginMode};
mod config;
mod plugins;
mod protocol;
mod network;
pub const VERSION: &str = std::env!("CARGO_PKG_VERSION");
fn main() {
let lua = Lua::new();
let plugin = Plugin::load("plugins/example_plugin/main.lua".into(), &lua).unwrap();
let mut plugins = Plugins::new(&lua).unwrap();
plugins.add_plugin(plugin);
env_logger::Builder::from_env(
Env::default().default_filter_or("info")
).format(|buf, record| {
let now = Utc::now().format("%Y-%m-%d %H:%M:%S");
let mut target = Cow::Borrowed(record.target());
if target.starts_with("quectocraft") {
target = Cow::Owned(record.target().replacen("quectocraft", "qc", 1));
}
let color = match record.level() {
log::Level::Error => "\x1b[31m",
log::Level::Warn => "\x1b[33m",
log::Level::Info => "\x1b[32m",
log::Level::Debug => "\x1b[37m",
log::Level::Trace => "\x1b[37m",
};
writeln!(buf, "\x1b[90m[\x1b[37m{} {color}{}\x1b[37m {}\x1b[90m]\x1b[0m {}", now, record.level(), target, record.args())
}).init();
let mut server = NetworkServer::new("127.0.0.1:25565".to_owned(), plugins);
info!("Starting Quectocraft version {}", VERSION);
let config = load_config().expect("Failed to load config");
match config.login {
LoginMode::Offline => warn!("Running in offline mode!"),
LoginMode::Velocity => info!("Running in velocity mode"),
}
let lua = Lua::new();
let mut plugins = Plugins::new(&lua).expect("Error initializing lua environment");
std::fs::create_dir_all("plugins").expect("Couldn't create the plugins directory");
plugins.load_plugins();
let mut server = NetworkServer::new(config, plugins);
let sleep_dur = Duration::from_millis(5);
let mut i = 0;
loop {
server.get_new_clients();
server.handle_connections();
std::thread::sleep(sleep_dur);
if i % 1024 == 0 {
server.send_keep_alive();
i = 0;
}
i += 1;
std::thread::sleep(sleep_dur);
}
}

56
src/network/client.rs Normal file
View File

@ -0,0 +1,56 @@
use std::{net::{TcpStream, Shutdown}, thread, io::Write, sync::mpsc::{Receiver, Sender, TryRecvError}, time::Duration};
use log::debug;
use crate::{protocol::{data::{PacketDecoder}, serverbound::*, clientbound::*, NetworkState}};
use super::Player;
pub struct NetworkClient {
pub id: i32,
pub verified: bool,
pub closed: bool,
pub stream: TcpStream,
pub serverbound: Receiver<ServerBoundPacket>,
pub player: Option<Player>,
}
impl NetworkClient {
pub fn listen(mut stream: TcpStream, send: Sender<ServerBoundPacket>) {
let mut state = NetworkState::Handshake;
let dur = Duration::from_millis(5);
loop {
match PacketDecoder::decode(&mut stream) {
Ok(decoder) => {
let packet = ServerBoundPacket::decode(&mut state, decoder);
send.send(packet).unwrap();
}
Err(_) => break
}
thread::sleep(dur)
}
let _ = stream.shutdown(Shutdown::Both);
}
pub fn recv_packet(&mut self, alive: &mut bool) -> Option<ServerBoundPacket> {
match self.serverbound.try_recv() {
Ok(packet) => Some(packet),
Err(TryRecvError::Empty) => None,
Err(TryRecvError::Disconnected) => {
*alive = false;
None
}
}
}
pub fn send_packet(&mut self, packet: impl ClientBoundPacket) -> std::io::Result<()> {
self.stream.write_all(&encode_packet(packet))?;
Ok(())
}
pub fn close(&mut self) {
debug!("Closed connection id {}", self.id);
let _ = self.stream.shutdown(Shutdown::Both);
self.closed = true;
}
}

View File

@ -1,243 +1,12 @@
use std::{net::{TcpStream, TcpListener, Shutdown}, thread, io::Write, sync::mpsc::{Receiver, Sender, channel, TryRecvError}, time::Duration};
use serde_json::json;
use uuid::Uuid;
use crate::{protocol::{data::{PacketDecoder, PacketEncoder}, serverbound::*, clientbound::*, NetworkState}, plugins::Plugins};
mod client;
mod server;
pub struct NetworkServer<'lua> {
plugins: Plugins<'lua>,
new_clients: Receiver<NetworkClient>,
clients: Vec<NetworkClient>,
}
impl <'lua> NetworkServer<'lua> {
pub fn new(addr: String, plugins: Plugins<'lua>) -> Self {
let (send, recv) = channel();
thread::spawn(move || Self::listen(&addr, send));
plugins.init();
Self {
plugins,
new_clients: recv,
clients: Vec::new(),
}
}
fn listen(addr: &str, send_clients: Sender<NetworkClient>) {
println!("listening for connections");
let listener = TcpListener::bind(addr).unwrap();
for (id, stream) in listener.incoming().enumerate() {
let stream = stream.unwrap();
println!("got connection from {} (id {})", stream.peer_addr().unwrap(), id);
let stream_2 = stream.try_clone().unwrap();
let (send, recv) = channel();
thread::spawn(|| NetworkClient::listen(stream_2, send));
let client = NetworkClient {
id: id as i32,
play: false,
closed: false,
stream,
serverbound: recv,
player: None,
};
send_clients.send(client).unwrap();
}
}
pub fn get_new_clients(&mut self) {
while let Ok(client) = self.new_clients.try_recv() {
self.clients.push(client);
}
}
pub fn send_keep_alive(&mut self) {
let mut closed = Vec::new();
for client in self.clients.iter_mut() {
if client.play {
if let Err(_) = client.send_packet(ClientBoundPacket::KeepAlive(0)) {
client.close();
if let Some(pl) = &client.player {
self.plugins.player_leave(pl);
}
closed.push(client.id);
}
}
}
self.clients.retain(|x| !closed.contains(&x.id));
}
pub fn handle_connections(&mut self) {
let mut closed = Vec::new();
for i in 0..self.clients.len() {
let client: &mut NetworkClient = unsafe {
&mut *(self.clients.get_unchecked_mut(i) as *mut _)
};
let mut alive = true;
while let Some(packet) = client.recv_packet(&mut alive) {
if let Err(_) = self.handle_packet(client, packet) {
alive = false;
break;
}
}
if !alive && !client.closed {
closed.push(client.id);
if let Some(pl) = &client.player {
self.plugins.player_leave(pl);
}
client.close();
}
}
self.clients.retain(|x| !closed.contains(&x.id));
}
fn handle_packet(&mut self, client: &mut NetworkClient, packet: ServerBoundPacket) -> std::io::Result<()> {
match packet {
ServerBoundPacket::Ignored(_) => (),
ServerBoundPacket::Unknown(id) => println!("unknown: {}", id),
ServerBoundPacket::Handshake(_) => (),
ServerBoundPacket::StatusRequest()
=> client.send_packet(ClientBoundPacket::StatusResponse(
r#"{"version":{"name":"1.19.2","protocol":760}}"#.to_owned()
))?,
ServerBoundPacket::PingRequest(n) => {
client.send_packet(ClientBoundPacket::PingResponse(n))?;
client.close();
}
ServerBoundPacket::LoginStart(login_start) => {
client.player = Some(Player {
name: login_start.name.clone(),
uuid: login_start.uuid.unwrap(),
});
client.play = true;
client.send_packet(ClientBoundPacket::LoginSuccess(LoginSuccess {
name: login_start.name,
uuid: login_start.uuid.unwrap(),
}))?;
self.plugins.player_join(client.player.as_ref().unwrap());
self.post_login(client)?;
}
ServerBoundPacket::ChatMessage(msg) => {
self.plugins.chat_message(client.player.as_ref().unwrap(), &msg.message);
}
}
Ok(())
}
fn post_login(&mut self, client: &mut NetworkClient) -> std::io::Result<()> {
client.send_packet(ClientBoundPacket::LoginPlay(LoginPlay {
eid: client.id,
is_hardcore: false,
gamemode: 1,
prev_gamemode: 1,
dimensions: vec![
"minecraft:world".to_owned(),
],
registry_codec: include_bytes!("../resources/registry_codec.nbt").to_vec(),
dimension_type: "minecraft:the_end".to_owned(),
dimension_name: "minecraft:world".to_owned(),
seed_hash: 0,
max_players: 0,
view_distance: 8,
sim_distance: 8,
reduced_debug_info: false,
respawn_screen: false,
is_debug: false,
is_flat: false,
death_location: None,
}))?;
client.send_packet(ClientBoundPacket::PluginMessage(PluginMessage {
channel: "minecraft:brand".to_owned(),
data: {
let mut data = Vec::new();
data.write_string(32767, "QuectoCraft");
data
}
}))?;
client.send_packet(ClientBoundPacket::PlayerAbilities(0x0d, 0.05, 0.1))?;
let mut chunk_data: Vec<u8> = Vec::new();
for _ in 0..(384 / 16) {
// number of non-air blocks
chunk_data.write_short(0);
// block states
chunk_data.write_ubyte(0);
chunk_data.write_varint(0);
chunk_data.write_varint(0);
// biomes
chunk_data.write_ubyte(0);
chunk_data.write_varint(0);
chunk_data.write_varint(0);
}
let hmdata = vec![0i64; 37];
let mut heightmap = nbt::Blob::new();
heightmap.insert("MOTION_BLOCKING", hmdata).unwrap();
client.send_packet(ClientBoundPacket::ChunkData(ChunkData {
x: 0,
z: 0,
heightmap,
chunk_data,
}))?;
client.send_packet(ClientBoundPacket::SyncPlayerPosition(SyncPlayerPosition {
x: 0.0,
y: 64.0,
z: 0.0,
yaw: 0.0,
pitch: 0.0,
flags: 0,
teleport_id: 0,
dismount: false
}))?;
Ok(())
}
}
pub use server::NetworkServer;
#[derive(Debug)]
pub struct Player {
pub name: String,
pub uuid: Uuid,
}
struct NetworkClient {
pub id: i32,
pub play: bool,
pub closed: bool,
stream: TcpStream,
serverbound: Receiver<ServerBoundPacket>,
player: Option<Player>,
}
impl NetworkClient {
pub fn listen(mut stream: TcpStream, send: Sender<ServerBoundPacket>) {
let mut state = NetworkState::Handshake;
let dur = Duration::from_millis(5);
loop {
if let Some(decoder) = PacketDecoder::decode(&mut stream) {
let packet = ServerBoundPacket::decode(&mut state, decoder);
send.send(packet).unwrap();
}
thread::sleep(dur)
}
}
pub fn recv_packet(&mut self, alive: &mut bool) -> Option<ServerBoundPacket> {
match self.serverbound.try_recv() {
Ok(packet) => Some(packet),
Err(TryRecvError::Empty) => None,
Err(TryRecvError::Disconnected) => {
*alive = false;
None
}
}
}
pub fn send_packet(&mut self, packet: ClientBoundPacket) -> std::io::Result<()> {
self.stream.write_all(&packet.encode())?;
Ok(())
}
pub fn close(&mut self) {
println!("closed connection with {}", self.id);
let _ = self.stream.shutdown(Shutdown::Both);
self.closed = true;
}
}

367
src/network/server.rs Normal file
View File

@ -0,0 +1,367 @@
use std::{net::{TcpListener, SocketAddr}, thread, sync::mpsc::{Receiver, Sender, channel}, collections::HashSet};
use hmac::{Hmac, Mac};
use log::{info, warn, debug, trace};
use serde_json::json;
use sha2::Sha256;
use crate::{protocol::{data::PacketEncoder, serverbound::*, clientbound::*, command::Commands, Position}, config::{Config, LoginMode}};
use crate::plugins::{Plugins, Response};
use crate::VERSION;
use super::{client::NetworkClient, Player};
pub struct NetworkServer<'lua> {
plugins: Plugins<'lua>,
commands: Commands,
new_clients: Receiver<NetworkClient>,
clients: Vec<NetworkClient>,
config: Config,
}
impl <'lua> NetworkServer<'lua> {
pub fn new(config: Config, mut plugins: Plugins<'lua>) -> Self {
let (send, recv) = channel();
info!("Initializing plugins");
plugins.init();
let mut commands = Commands::new();
commands.create_simple_cmd("qc");
let commands = plugins.register_commands(commands).unwrap();
thread::spawn(move || Self::listen(&SocketAddr::new(config.addr, config.port), send));
Self {
config,
plugins,
commands,
new_clients: recv,
clients: Vec::new(),
}
}
fn listen(addr: &SocketAddr, send_clients: Sender<NetworkClient>) {
info!("Listening on {}", addr);
let listener = TcpListener::bind(addr).unwrap();
for (id, stream) in listener.incoming().enumerate() {
let stream = stream.unwrap();
debug!("Connection from {} (id {})", stream.peer_addr().unwrap(), id);
let stream_2 = stream.try_clone().unwrap();
let (send, recv) = channel();
thread::spawn(|| NetworkClient::listen(stream_2, send));
let client = NetworkClient {
id: id as i32,
verified: false,
closed: false,
stream,
serverbound: recv,
player: None,
};
send_clients.send(client).unwrap();
}
}
pub fn get_new_clients(&mut self) {
while let Ok(client) = self.new_clients.try_recv() {
self.clients.push(client);
}
}
pub fn send_keep_alive(&mut self) {
let mut closed = Vec::new();
for client in self.clients.iter_mut() {
if client.player.is_some() {
let result = client.send_packet(KeepAlive { data: 0 });
if result.is_err() {
client.close();
self.plugins.player_leave(client.player.as_ref().unwrap());
closed.push(client.id);
}
}
}
self.clients.retain(|x| !closed.contains(&x.id));
}
pub fn handle_connections(&mut self) {
let mut closed = HashSet::new();
for i in 0..self.clients.len() {
let client: &mut NetworkClient = unsafe {
&mut *(self.clients.get_unchecked_mut(i) as *mut _)
};
let mut alive = true;
while let Some(packet) = client.recv_packet(&mut alive) {
let result = self.handle_packet(client, packet);
if result.is_err() {
warn!("error: {}", result.unwrap_err());
alive = false;
break
}
}
if !alive && !client.closed {
closed.insert(client.id);
if let Some(pl) = &client.player {
self.plugins.player_leave(pl);
}
client.close();
}
}
for response in self.plugins.get_responses() {
let _ = self.handle_plugin_response(response);
}
self.clients.retain(|x| !closed.contains(&x.id) && !x.closed);
}
fn handle_plugin_response(&mut self, response: Response) -> std::io::Result<()> {
match response {
Response::Message { player, message } => {
for client in self.clients.iter_mut() {
if let Some(p) = &client.player {
if p.name == player || p.uuid.to_string() == player {
client.send_packet(SystemChatMessage { message, overlay: false })?;
break
}
}
}
},
Response::Broadcast { message } => {
for client in self.clients.iter_mut() {
if client.player.is_some() {
client.send_packet(SystemChatMessage { message: message.clone(), overlay: false })?;
}
}
},
Response::Disconnect { player, reason } => {
for client in self.clients.iter_mut() {
if let Some(pl) = &client.player {
if pl.name == player || pl.uuid.to_string() == player {
client.send_packet(Disconnect { reason: reason.clone() })?;
}
}
}
},
Response::PluginMessage { player, channel, data } => {
for client in self.clients.iter_mut() {
if let Some(pl) = &client.player {
if pl.name == player || pl.uuid.to_string() == player {
client.send_packet(CPluginMessage { channel: channel.clone(), data: data.clone() })?;
}
}
}
}
,
}
Ok(())
}
fn handle_packet(&mut self, client: &mut NetworkClient, packet: ServerBoundPacket) -> Result<(), Box<dyn std::error::Error>> {
trace!("Recieved packet from client {}:", client.id);
match packet {
ServerBoundPacket::Ignored(_) => (),
ServerBoundPacket::Unknown(id) => warn!("Unknown packet: {}", id),
ServerBoundPacket::Handshake(_) => (),
ServerBoundPacket::StatusRequest()
=> client.send_packet(StatusResponse {
data: r#"{"version":{"name":"1.19.3","protocol":761}}"#.to_owned()
})?,
ServerBoundPacket::PingRequest(n) => {
client.send_packet(PingResponse { data: n })?;
client.close();
}
ServerBoundPacket::LoginStart(login_start)
=> self.start_login(client, login_start)?,
ServerBoundPacket::LoginPluginResponse(LoginPluginResponse { id: 10, data })
=> self.velocity_login(client, data)?,
ServerBoundPacket::LoginPluginResponse(LoginPluginResponse{ id: -1, .. })
=> self.login(client)?,
ServerBoundPacket::LoginPluginResponse { .. } => {
client.send_packet(LoginDisconnect { reason: json!({"text": "bad plugin response"}) })?;
client.close();
}
ServerBoundPacket::ChatMessage(msg) => {
self.plugins.chat_message(client.player.as_ref().unwrap(), &msg.message);
}
ServerBoundPacket::ChatCommand(msg) => {
let mut parts = msg.message.splitn(2, ' ');
if let Some(cmd) = parts.next() {
if cmd == "qc" {
client.send_packet(SystemChatMessage { message: json!({
"text": format!("QuectoCraft version {}", VERSION),
"color": "green"
}), overlay: false })?;
} else {
let args = parts.next().unwrap_or_default();
self.plugins.command(client.player.as_ref().unwrap(), cmd, args);
}
}
}
ServerBoundPacket::PluginMessage(SPluginMessage { channel, data }) => {
self.plugins.plugin_message(client.player.as_ref().unwrap(), &channel, &data);
}
}
Ok(())
}
fn start_login(&mut self, client: &mut NetworkClient, login_start: LoginStart) -> Result<(), Box<dyn std::error::Error>> {
if self.clients.iter().filter_map(|x| x.player.as_ref()).any(|x| x.uuid == login_start.uuid) {
client.send_packet(LoginDisconnect { reason: json!({
"translate": "multiplayer.disconnect.duplicate_login"
})})?;
client.close();
return Ok(())
}
client.player = Some(Player {
name: login_start.name.clone(),
uuid: login_start.uuid,
});
match self.config.login {
LoginMode::Offline => {
client.verified = true;
client.send_packet(LoginPluginRequest{
id: -1,
channel: "qc:init".to_owned(),
data: Vec::new()
})?;
},
LoginMode::Velocity => {
client.send_packet(LoginPluginRequest{
id: 10,
channel: "velocity:player_info".to_owned(),
data: vec![1],
})?
}
}
Ok(())
}
fn velocity_login(&mut self, client: &mut NetworkClient, data: Option<Vec<u8>>) -> Result<(), Box<dyn std::error::Error>> {
let Some(data) = data else {
client.send_packet(LoginDisconnect { reason: json!({
"text": "This server can only be connected to via a Velocity proxy",
"color": "red"
})})?;
client.close();
return Ok(());
};
let (sig, data) = data.split_at(32);
let mut mac = Hmac::<Sha256>::new_from_slice(self.config.velocity_secret.clone().unwrap().as_bytes())?;
mac.update(data);
if mac.verify_slice(sig).is_err() {
client.send_packet(Disconnect { reason: json!({
"text": "Could not verify secret. Ensure that the secrets configured for Velocity and Quectocraft match."
})})?;
client.close();
return Ok(())
}
client.verified = true;
client.send_packet(LoginPluginRequest{
id: -1,
channel: "qc:init".to_owned(),
data: Vec::new()
})?;
Ok(())
}
//
// Handle the end of "login" and beginning of "play"
//
fn login(&mut self, client: &mut NetworkClient) -> std::io::Result<()> {
if !client.verified {
client.send_packet(Disconnect { reason: json!({
"text": "Failed to verify your connection",
"color": "red",
})})?;
client.close();
return Ok(())
}
client.send_packet(LoginSuccess {
name: client.player.as_ref().unwrap().name.to_owned(),
uuid: client.player.as_ref().unwrap().uuid,
})?;
self.plugins.player_join(client.player.as_ref().unwrap());
client.send_packet(LoginPlay {
eid: client.id,
is_hardcore: false,
gamemode: 3,
prev_gamemode: 3,
dimensions: vec![
"qc:world".to_owned(),
],
registry_codec: include_bytes!("../resources/registry_codec.nbt").to_vec(),
dimension_type: "minecraft:the_end".to_owned(),
dimension_name: "qc:world".to_owned(),
seed_hash: 0,
max_players: 0,
view_distance: 8,
sim_distance: 8,
reduced_debug_info: false,
respawn_screen: false,
is_debug: false,
is_flat: false,
death_location: None,
})?;
client.send_packet(CPluginMessage {
channel: "minecraft:brand".to_owned(),
data: {
let mut data = Vec::new();
data.write_string(32767, &format!("Quectocraft {}", VERSION));
data
}
})?;
client.send_packet(self.commands.clone())?;
// Send 3x3 square of empty chunks.
let mut chunk_data: Vec<u8> = Vec::new();
for _ in 0..(384 / 16) {
// number of non-air blocks
chunk_data.write_short(0);
// block states
chunk_data.write_ubyte(0);
chunk_data.write_varint(0);
chunk_data.write_varint(0);
// biomes
chunk_data.write_ubyte(0);
chunk_data.write_varint(0);
chunk_data.write_varint(0);
}
let hmdata = vec![0i64; 37];
let mut heightmap = nbt::Blob::new();
heightmap.insert("MOTION_BLOCKING", hmdata).unwrap();
for x in -1..=1 {
for z in -1..=1 {
client.send_packet(ChunkData {
x,
z,
heightmap: heightmap.clone(),
chunk_data: chunk_data.clone(),
})?;
}
}
client.send_packet(SetDefaultSpawnPosition {
pos: Position { x: 0, y: 0, z: 0 }, angle: 0.0
})?;
client.send_packet(SyncPlayerPosition {
x: 0.0,
y: 64.0,
z: 0.0,
yaw: 0.0,
pitch: 0.0,
flags: 0,
teleport_id: 0,
dismount: false
})?;
Ok(())
}
}

View File

@ -1,128 +0,0 @@
use std::{path::{Path, PathBuf}, str::FromStr};
use mlua::{Function, Lua, Error, Table};
use uuid::Uuid;
use crate::network::Player;
pub struct EventHandlers<'lua> {
init: Option<Function<'lua>>,
player_join: Option<Function<'lua>>,
player_leave: Option<Function<'lua>>,
chat_message: Option<Function<'lua>>,
}
pub struct Plugin<'lua> {
pub id: String,
pub name: String,
pub version: String,
pub event_handlers: EventHandlers<'lua>,
}
impl <'lua> Plugin<'lua> {
pub fn load(path: &str, lua: &'lua Lua) -> Result<Self, Box<dyn std::error::Error>> {
let path = PathBuf::from_str(path).unwrap();
let chunk = lua.load(&path);
let module: Table = chunk.eval()?;
let id: String = module.get("id")?;
let name: String = module.get("name").unwrap_or_else(|_| id.clone());
let version: String = module.get("version").unwrap_or_else(|_| "?".to_owned());
let init: Option<Function<'lua>> = module.get("init").ok();
let player_join: Option<Function<'lua>> = module.get("playerJoin").ok();
let player_leave: Option<Function<'lua>> = module.get("playerLeave").ok();
let chat_message: Option<Function<'lua>> = module.get("chatMessage").ok();
let event_handlers = EventHandlers { init, player_join, player_leave, chat_message };
Ok(Plugin { id, name, version, event_handlers })
}
}
pub struct Plugins<'lua> {
lua: &'lua Lua,
plugins: Vec<Plugin<'lua>>
}
impl <'lua> Plugins<'lua> {
pub fn new(lua: &'lua Lua) -> Result<Self, mlua::Error> {
let server = lua.create_table()?;
let players = lua.create_table()?;
server.set("players", players)?;
let fn_send = lua.create_function(|_, (uuid, message): (String, String)| {
Ok(())
})?;
server.set("send", fn_send)?;
lua.globals().set("server", server)?;
Ok(Self {
lua,
plugins: Vec::new(),
})
}
pub fn add_plugin(&mut self, pl: Plugin<'lua>) {
self.plugins.push(pl);
}
pub fn init(&self) {
for pl in &self.plugins {
if let Some(init) = &pl.event_handlers.init {
if let Err(e) = init.call::<_, ()>(()) {
println!("Error in plugin {}: {}", pl.name, e);
}
}
}
}
pub fn player_join(&self, player: &Player) {
if let Err(e) = self.add_player(player) {
println!("Error adding player: {}", e);
return
}
for pl in &self.plugins {
if let Some(init) = &pl.event_handlers.player_join {
if let Err(e) = init.call::<_, ()>((player.name.as_str(), player.uuid.to_string())) {
println!("Error in plugin {}: {}", pl.name, e);
}
}
}
}
fn add_player(&self, player: &Player) -> Result<(), mlua::Error> {
let server: Table = self.lua.globals().get("server")?;
let players: Table = server.get("players")?;
players.set(player.uuid.to_string(), player.name.as_str())?;
Ok(())
}
pub fn player_leave(&self, player: &Player) {
if let Err(e) = self.remove_player(player.uuid) {
println!("Error removing player: {}", e);
return
}
for pl in &self.plugins {
if let Some(func) = &pl.event_handlers.player_leave {
if let Err(e) = func.call::<_, ()>((player.name.as_str(), player.uuid.to_string())) {
println!("Error in plugin {}: {}", pl.name, e);
}
}
}
}
fn remove_player(&self, uuid: Uuid) -> Result<(), mlua::Error> {
let server: Table = self.lua.globals().get("server")?;
let players: Table = server.get("players")?;
players.set(uuid.to_string(), mlua::Nil)?;
Ok(())
}
pub fn chat_message(&self, player: &Player, message: &str) {
for pl in &self.plugins {
if let Some(func) = &pl.event_handlers.chat_message {
if let Err(e) = func.call::<_, ()>((message, player.name.as_str(), player.uuid.to_string())) {
println!("Error in plugin {}: {}", pl.name, e);
}
}
}
}
}

48
src/plugins/init.lua Normal file
View File

@ -0,0 +1,48 @@
server = { players = {} }
_qc = { responses = {} }
local function to_chat(message, default)
if message == nil then
if default ~= nil then
return default
else
error("message must be a string or table")
end
elseif type(message) == "table" then
return message
elseif type(message) == "string" then
return { text = message }
elseif default == nil then
error("message must be a string or table")
else
error("message must be a string, table, or nil for the default message")
end
end
function server.sendMessage(player, message)
if type(player) ~= "string" then
error("player must be a string")
end
local message = assert(to_chat(message))
table.insert(_qc.responses, {type = "message", player = player, message = message})
end
function server.broadcast(message)
local message = assert(to_chat(message))
table.insert(_qc.responses, { type = "broadcast", message = message })
end
function server.sendPluginMessage(player, channel, data)
if type(player) ~= "string" then
error("player must be a string")
end
if type(channel) ~= "string" then
error("channel must be a string")
end
table.insert(_qc.responses, {type = "plugin_message", player = player, channel = channel, data = data})
end
function server.disconnect(player, reason)
local reason = assert(to_chat(reason, { translate = "multiplayer.disconnect.generic" }))
table.insert(_qc.responses, { type = "disconnect", player = player, reason = reason })
end

37
src/plugins/init_lua.rs Normal file
View File

@ -0,0 +1,37 @@
use log::{info, warn, trace, error, debug};
use mlua::{Lua, chunk};
use crate::VERSION;
pub fn init(lua: &Lua) -> Result<(), mlua::Error> {
macro_rules! log_any {
($level:tt) => {
lua.create_function(|_, args: (String, String)| {
$level!(target: &args.0, "{}", args.1);
Ok(())
})
}
}
let log_trace = log_any!(trace)?;
let log_debug = log_any!(debug)?;
let log_info = log_any!(info)?;
let log_warn = log_any!(warn)?;
let log_error = log_any!(error)?;
lua.load(include_str!("init.lua")).exec()?;
lua.load(chunk!{
function server.initLogger(plugin)
local id = "pl::" .. assert(plugin["id"])
return {
trace = function(msg) $log_trace(id, msg) end,
debug = function(msg) $log_debug(id, msg) end,
info = function(msg) $log_info(id, msg) end,
warn = function(msg) $log_warn(id, msg) end,
error = function(msg) $log_error(id, msg) end,
}
end
server.version = $VERSION
}).exec()?;
Ok(())
}

195
src/plugins/mod.rs Normal file
View File

@ -0,0 +1,195 @@
use std::{fs::read_dir, rc::Rc, cell::RefCell, collections::HashMap};
use log::{warn, info};
use mlua::{Lua, Table, LuaSerdeExt};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::{network::Player, protocol::command::Commands};
use self::plugin::Plugin;
mod init_lua;
mod plugin;
#[derive(Serialize, Deserialize)]
#[serde(tag="type")]
pub enum Response {
#[serde(rename = "message")]
Message { player: String, message: serde_json::Value },
#[serde(rename = "plugin_message")]
PluginMessage { player: String, channel: String, data: Vec<u8> },
#[serde(rename = "broadcast")]
Broadcast { message: serde_json::Value },
#[serde(rename = "disconnect")]
Disconnect { player: String, reason: serde_json::Value },
}
pub struct Plugins<'lua> {
lua: &'lua Lua,
plugins: Vec<Plugin<'lua>>,
cmd_owners: HashMap<String, usize>,
}
impl <'lua> Plugins<'lua> {
pub fn new(lua: &'lua Lua) -> Result<Self, mlua::Error> {
init_lua::init(lua)?;
Ok(Self {
lua,
plugins: Vec::new(),
cmd_owners: HashMap::new(),
})
}
pub fn load_plugins(&mut self) {
let files = read_dir("plugins").expect("couldn't read plugins directory");
for file in files {
let file = file.expect("couldn't read contents of plugins directory");
let path = if file.file_type().expect("couldn't get type of plugin file").is_dir() {
let mut main = file.path();
main.push("main.lua");
main
} else {
file.path()
};
let pl = Plugin::load(&path, self.lua).expect("error loading plugin");
self.plugins.push(pl);
info!("Loaded plugin '{}'", file.file_name().to_string_lossy());
}
}
pub fn get_responses(&self) -> Vec<Response> {
match self.get_responses_inner() {
Ok(x) => x,
Err(e) => {
warn!("Error getting responses: {}", e);
Vec::new()
}
}
}
fn get_responses_inner(&self) -> Result<Vec<Response>, Box<dyn std::error::Error>> {
let qc: Table = self.lua.globals().get("_qc")?;
let responses: Vec<Response> = self.lua.from_value(qc.get("responses")?)?;
qc.set("responses", self.lua.create_table()?)?;
Ok(responses)
}
pub fn init(&self) {
for pl in &self.plugins {
if let Some(init) = &pl.event_handlers.init {
if let Err(e) = init.call::<_, ()>(()) {
warn!("Error in plugin {}: {}", pl.name, e);
}
}
}
}
pub fn register_commands(&mut self, commands: Commands) -> Result<Commands, mlua::Error> {
let commands = Rc::new(RefCell::new(commands));
let cmd_owners = Rc::new(RefCell::new(HashMap::new()));
for (i, pl) in self.plugins.iter().enumerate() {
let commands_2 = commands.clone();
let cmd_owners_2 = cmd_owners.clone();
let pl_id = pl.id.clone();
let add_command = self.lua.create_function(move |_, name: String| {
let scoped_name = format!("{}:{}", pl_id, name);
let mut cmds = commands_2.borrow_mut();
let id1 = cmds.create_simple_cmd(&name);
let id2 = cmds.create_simple_cmd(&scoped_name);
if id1.is_none() || id2.is_none() {
return Ok(mlua::Nil)
}
cmd_owners_2.borrow_mut().insert(name, i);
cmd_owners_2.borrow_mut().insert(scoped_name, i);
Ok(mlua::Nil)
})?;
let registry = self.lua.create_table()?;
registry.set("addCommand", add_command)?;
if let Some(init) = &pl.event_handlers.register_commands {
if let Err(e) = init.call::<_, ()>((registry.clone(),)) {
warn!("Error in plugin {}: {}", pl.name, e);
}
}
}
let cb = commands.borrow();
self.cmd_owners = (*cmd_owners.borrow()).clone();
Ok((*cb).clone())
}
pub fn player_join(&self, player: &Player) {
if let Err(e) = self.add_player(player) {
warn!("Error adding player: {}", e);
return
}
for pl in &self.plugins {
if let Some(init) = &pl.event_handlers.player_join {
if let Err(e) = init.call::<_, ()>((player.name.as_str(), player.uuid.to_string())) {
warn!("Error in plugin {}: {}", pl.name, e);
}
}
}
}
fn add_player(&self, player: &Player) -> Result<(), mlua::Error> {
let server: Table = self.lua.globals().get("server")?;
let players: Table = server.get("players")?;
players.set(player.uuid.to_string(), player.name.as_str())?;
Ok(())
}
pub fn player_leave(&self, player: &Player) {
if let Err(e) = self.remove_player(player.uuid) {
warn!("Error removing player: {}", e);
return
}
for pl in &self.plugins {
if let Some(func) = &pl.event_handlers.player_leave {
if let Err(e) = func.call::<_, ()>((player.name.as_str(), player.uuid.to_string())) {
warn!("Error in plugin {}: {}", pl.name, e);
}
}
}
}
fn remove_player(&self, uuid: Uuid) -> Result<(), mlua::Error> {
let server: Table = self.lua.globals().get("server")?;
let players: Table = server.get("players")?;
players.set(uuid.to_string(), mlua::Nil)?;
Ok(())
}
pub fn chat_message(&self, player: &Player, message: &str) {
for pl in &self.plugins {
if let Some(func) = &pl.event_handlers.chat_message {
if let Err(e) = func.call::<_, ()>((message, player.name.as_str(), player.uuid.to_string())) {
warn!("Error in plugin {}: {}", pl.name, e);
}
}
}
}
pub fn command(&self, player: &Player, command: &str, args: &str) {
if let Some(owner) = self.cmd_owners.get(command) {
let pl = &self.plugins[*owner];
if let Some(func) = &pl.event_handlers.command {
if let Err(e) = func.call::<_, ()>((command, args, player.name.as_str(), player.uuid.to_string())) {
warn!("Error in plugin {}: {}", pl.name, e);
}
} else {
warn!("Plugin {} registered a command but no command handler was found", pl.id);
}
}
}
pub fn plugin_message(&self, player: &Player, channel: &str, data: &[u8]) {
for pl in &self.plugins {
if let Some(func) = &pl.event_handlers.plugin_message {
if let Err(e) = func.call::<_, ()>((channel, data, player.name.as_str(), player.uuid.to_string())) {
warn!("Error in plugin {}: {}", pl.name, e);
}
}
}
}
}

50
src/plugins/plugin.rs Normal file
View File

@ -0,0 +1,50 @@
use std::path::Path;
use mlua::{Function, Table, Lua};
pub struct EventHandlers<'lua> {
pub init: Option<Function<'lua>>,
pub register_commands: Option<Function<'lua>>,
pub player_join: Option<Function<'lua>>,
pub player_leave: Option<Function<'lua>>,
pub chat_message: Option<Function<'lua>>,
pub command: Option<Function<'lua>>,
pub plugin_message: Option<Function<'lua>>,
}
pub struct Plugin<'lua> {
pub id: String,
pub name: String,
pub version: String,
pub event_handlers: EventHandlers<'lua>,
}
impl <'lua> Plugin<'lua> {
pub fn load(path: &Path, lua: &'lua Lua) -> Result<Self, Box<dyn std::error::Error>> {
let chunk = lua.load(path);
let module: Table = chunk.eval()?;
let id: String = module.get("id")?;
let name: String = module.get("name").unwrap_or_else(|_| id.clone());
let version: String = module.get("version").unwrap_or_else(|_| "?".to_owned());
let init: Option<Function<'lua>> = module.get("init").ok();
let register_commands: Option<Function<'lua>> = module.get("registerCommands").ok();
let player_join: Option<Function<'lua>> = module.get("playerJoin").ok();
let player_leave: Option<Function<'lua>> = module.get("playerLeave").ok();
let chat_message: Option<Function<'lua>> = module.get("chatMessage").ok();
let command: Option<Function<'lua>> = module.get("command").ok();
let plugin_message: Option<Function<'lua>> = module.get("pluginMessage").ok();
let event_handlers = EventHandlers {
init,
register_commands,
player_join,
player_leave,
chat_message,
command,
plugin_message,
};
Ok(Plugin { id, name, version, event_handlers })
}
}

View File

@ -2,20 +2,102 @@ use uuid::Uuid;
use super::{data::{PacketEncoder, finalize_packet}, Position};
pub trait ClientBoundPacket: std::fmt::Debug {
fn encode(&self, encoder: &mut impl PacketEncoder);
fn packet_id(&self) -> i32;
}
//////////////////
// //
// Status //
// //
//////////////////
#[derive(Debug)]
pub struct PingResponse {
pub data: i64
}
impl ClientBoundPacket for PingResponse {
fn encode(&self, encoder: &mut impl PacketEncoder) {
encoder.write_long(self.data)
}
fn packet_id(&self) -> i32 { 0x01 }
}
#[derive(Debug)]
pub struct StatusResponse {
pub data: String
}
impl ClientBoundPacket for StatusResponse {
fn encode(&self, encoder: &mut impl PacketEncoder) {
encoder.write_string(32767, &self.data)
}
fn packet_id(&self) -> i32 { 0x00 }
}
/////////////////
// //
// Login //
// //
/////////////////
#[derive(Debug)]
pub struct LoginSuccess {
pub uuid: Uuid,
pub name: String,
}
impl LoginSuccess {
pub fn encode(self, encoder: &mut impl PacketEncoder) {
impl ClientBoundPacket for LoginSuccess {
fn encode(&self, encoder: &mut impl PacketEncoder) {
encoder.write_uuid(self.uuid);
encoder.write_string(16, &self.name);
encoder.write_varint(0);
}
fn packet_id(&self) -> i32 { 0x02 }
}
#[derive(Debug)]
pub struct LoginPluginRequest {
pub id: i32,
pub channel: String,
pub data: Vec<u8>,
}
impl ClientBoundPacket for LoginPluginRequest {
fn encode(&self, encoder: &mut impl PacketEncoder) {
encoder.write_varint(self.id);
encoder.write_string(32767, &self.channel);
encoder.write_bytes(&self.data);
}
fn packet_id(&self) -> i32 { 0x04 }
}
#[derive(Debug)]
pub struct LoginDisconnect {
pub reason: serde_json::Value
}
impl ClientBoundPacket for LoginDisconnect {
fn encode(&self, encoder: &mut impl PacketEncoder) {
encoder.write_string(262144, &self.reason.to_string())
}
fn packet_id(&self) -> i32 { 0x00 }
}
////////////////
// //
// Play //
// //
////////////////
#[derive(Debug)]
pub struct LoginPlay {
pub eid: i32,
@ -37,15 +119,15 @@ pub struct LoginPlay {
pub death_location: Option<(String, Position)>
}
impl LoginPlay {
pub fn encode(self, encoder: &mut impl PacketEncoder) {
impl ClientBoundPacket for LoginPlay {
fn encode(&self, encoder: &mut impl PacketEncoder) {
encoder.write_int(self.eid);
encoder.write_bool(self.is_hardcore);
encoder.write_ubyte(self.gamemode);
encoder.write_ubyte(self.prev_gamemode);
encoder.write_varint(self.dimensions.len() as i32);
for dim in self.dimensions {
encoder.write_string(32767, &dim);
for dim in &self.dimensions {
encoder.write_string(32767, dim);
}
encoder.write_bytes(&self.registry_codec);
encoder.write_string(32767, &self.dimension_type);
@ -59,24 +141,28 @@ impl LoginPlay {
encoder.write_bool(self.is_debug);
encoder.write_bool(self.is_flat);
encoder.write_bool(self.death_location.is_some());
if let Some(dl) = self.death_location {
if let Some(dl) = &self.death_location {
encoder.write_string(32767, &dl.0);
encoder.write_position(dl.1);
}
}
fn packet_id(&self) -> i32 { 0x24 }
}
#[derive(Debug)]
pub struct PluginMessage {
pub struct CPluginMessage {
pub channel: String,
pub data: Vec<u8>,
}
impl PluginMessage {
pub fn encode(self, encoder: &mut impl PacketEncoder) {
impl ClientBoundPacket for CPluginMessage {
fn encode(&self, encoder: &mut impl PacketEncoder) {
encoder.write_string(32767, &self.channel);
encoder.write_bytes(&self.data);
}
fn packet_id(&self) -> i32 { 0x15 }
}
#[derive(Debug)]
@ -91,8 +177,8 @@ pub struct SyncPlayerPosition {
pub dismount: bool,
}
impl SyncPlayerPosition {
pub fn encode(self, encoder: &mut impl PacketEncoder) {
impl ClientBoundPacket for SyncPlayerPosition {
fn encode(&self, encoder: &mut impl PacketEncoder) {
encoder.write_double(self.x);
encoder.write_double(self.y);
encoder.write_double(self.z);
@ -102,6 +188,8 @@ impl SyncPlayerPosition {
encoder.write_varint(self.teleport_id);
encoder.write_bool(self.dismount);
}
fn packet_id(&self) -> i32 { 0x38 }
}
#[derive(Debug)]
@ -112,8 +200,8 @@ pub struct ChunkData {
pub chunk_data: Vec<u8>,
}
impl ChunkData {
pub fn encode(self, encoder: &mut impl PacketEncoder) {
impl ClientBoundPacket for ChunkData {
fn encode(&self, encoder: &mut impl PacketEncoder) {
encoder.write_int(self.x);
encoder.write_int(self.z);
self.heightmap.to_writer(encoder).unwrap();
@ -133,72 +221,86 @@ impl ChunkData {
// block light array
encoder.write_varint(0);
}
fn packet_id(&self) -> i32 { 0x20 }
}
#[derive(Debug)]
pub enum ClientBoundPacket {
// status
StatusResponse(String),
PingResponse(i64),
// login
LoginSuccess(LoginSuccess),
// play
LoginPlay(LoginPlay),
PluginMessage(PluginMessage),
SyncPlayerPosition(SyncPlayerPosition),
ChunkData(ChunkData),
KeepAlive(i64),
PlayerAbilities(i8, f32, f32),
SystemChatMessage(serde_json::Value, bool),
pub struct KeepAlive {
pub data: i64
}
impl ClientBoundPacket {
pub fn encode(self) -> Vec<u8> {
let mut packet = Vec::new();
match self {
Self::StatusResponse(status) => {
packet.write_string(32767, &status);
finalize_packet(packet, 0)
},
Self::PingResponse(n) => {
packet.write_long(n);
finalize_packet(packet, 1)
},
Self::LoginSuccess(login_success) => {
login_success.encode(&mut packet);
finalize_packet(packet, 2)
}
Self::PluginMessage(plugin_message) => {
plugin_message.encode(&mut packet);
finalize_packet(packet, 22)
}
Self::SyncPlayerPosition(sync_player_position) => {
sync_player_position.encode(&mut packet);
finalize_packet(packet, 57)
}
Self::LoginPlay(login_play) => {
login_play.encode(&mut packet);
finalize_packet(packet, 37)
}
Self::ChunkData(chunk_data) => {
chunk_data.encode(&mut packet);
finalize_packet(packet, 33)
}
Self::KeepAlive(n) => {
packet.write_long(n);
finalize_packet(packet, 32)
}
Self::PlayerAbilities(flags, speed, view) => {
packet.write_byte(flags);
packet.write_float(speed);
packet.write_float(view);
finalize_packet(packet, 49)
}
Self::SystemChatMessage(msg, overlay) => {
packet.write_string(262144, &msg.to_string());
packet.write_bool(overlay);
finalize_packet(packet, 98)
}
}
impl ClientBoundPacket for KeepAlive {
fn encode(&self, encoder: &mut impl PacketEncoder) {
encoder.write_long(self.data)
}
fn packet_id(&self) -> i32 { 0x1f }
}
#[derive(Debug)]
pub struct PlayerAbilities {
pub flags: i8,
pub fly_speed: f32,
pub fov_modifier: f32,
}
impl ClientBoundPacket for PlayerAbilities {
fn encode(&self, encoder: &mut impl PacketEncoder) {
encoder.write_byte(self.flags);
encoder.write_float(self.fly_speed);
encoder.write_float(self.fov_modifier);
}
fn packet_id(&self) -> i32 { 0x30 }
}
#[derive(Debug)]
pub struct Disconnect {
pub reason: serde_json::Value
}
impl ClientBoundPacket for Disconnect {
fn encode(&self, encoder: &mut impl PacketEncoder) {
encoder.write_string(262144, &self.reason.to_string())
}
fn packet_id(&self) -> i32 { 0x17 }
}
#[derive(Debug)]
pub struct SetDefaultSpawnPosition {
pub pos: Position,
pub angle: f32
}
impl ClientBoundPacket for SetDefaultSpawnPosition {
fn encode(&self, encoder: &mut impl PacketEncoder) {
encoder.write_position(self.pos);
encoder.write_float(self.angle);
}
fn packet_id(&self) -> i32 { 0x4c }
}
#[derive(Debug)]
pub struct SystemChatMessage {
pub message: serde_json::Value,
pub overlay: bool
}
impl ClientBoundPacket for SystemChatMessage {
fn encode(&self, encoder: &mut impl PacketEncoder) {
encoder.write_string(262144, &self.message.to_string());
encoder.write_bool(self.overlay);
}
fn packet_id(&self) -> i32 { 0x60 }
}
pub fn encode_packet(packet: impl ClientBoundPacket) -> Vec<u8> {
let mut buffer = Vec::new();
packet.encode(&mut buffer);
finalize_packet(buffer, packet.packet_id())
}

192
src/protocol/command.rs Normal file
View File

@ -0,0 +1,192 @@
use super::{data::PacketEncoder, clientbound::ClientBoundPacket};
#[derive(Debug, Clone)]
pub struct Commands {
nodes: Vec<CommandNode>,
}
impl ClientBoundPacket for Commands {
fn encode(&self, encoder: &mut impl PacketEncoder) {
encoder.write_varint(self.nodes.len() as i32);
for node in &self.nodes {
node.encode(encoder);
}
// root node
encoder.write_varint(0);
}
fn packet_id(&self) -> i32 { 0x0e }
}
impl Commands {
pub fn new() -> Self {
let root = CommandNode {
executable: false,
redirect: None,
children: Vec::new(),
suggestion: None,
type_data: CommandNodeType::Root
};
let simple_cmd_arg = CommandNode {
executable: true,
redirect: None,
children: Vec::new(),
suggestion: None,
type_data: CommandNodeType::Argument { name: "[args]".to_owned(), parser: Parser::String { kind: StringKind::Greedy } }
};
Self {
nodes: vec![root, simple_cmd_arg],
}
}
pub fn create_node(
&mut self,
parent: i32,
type_data: CommandNodeType,
executable: bool,
redirect: Option<i32>,
suggestion: Option<String>
) -> Option<i32> {
if parent < 0 || parent >= self.nodes.len() as i32 {
return None
}
if let Some(redirect) = redirect {
if redirect < 0 || redirect >= self.nodes.len() as i32 {
return None
}
}
if let CommandNodeType::Root = type_data {
return None
}
let id = self.nodes.len() as i32;
self.nodes.push(CommandNode {
executable,
redirect,
children: Vec::new(),
suggestion,
type_data,
});
self.nodes[parent as usize].children.push(id);
Some(id)
}
pub fn create_simple_cmd(&mut self, name: &str) -> Option<i32> {
let id = self.create_node(0, CommandNodeType::Literal { name: name.to_owned() }, true, None, None)?;
self.add_child(id, 1);
Some(id)
}
pub fn add_child(&mut self, node: i32, child: i32) {
self.nodes[node as usize].children.push(child);
}
}
#[derive(Debug, Clone)]
pub struct CommandNode {
executable: bool,
redirect: Option<i32>,
children: Vec<i32>,
suggestion: Option<String>,
type_data: CommandNodeType,
}
#[derive(Debug, Clone)]
pub enum CommandNodeType {
Root,
Literal { name: String },
Argument { name: String, parser: Parser }
}
impl CommandNode {
pub fn encode(&self, encoder: &mut impl PacketEncoder) {
let mut flags = match self.type_data {
CommandNodeType::Root => 0,
CommandNodeType::Literal{..} => 1,
CommandNodeType::Argument{..} => 2,
};
if self.executable { flags |= 4; }
if self.redirect.is_some() { flags |= 8; }
if self.suggestion.is_some() { flags |= 16; }
encoder.write_byte(flags);
encoder.write_varint(self.children.len() as i32);
for child in &self.children {
encoder.write_varint(*child);
}
if let Some(redirect) = &self.redirect {
encoder.write_varint(*redirect);
}
match &self.type_data {
CommandNodeType::Root => (),
CommandNodeType::Literal { name } => {
encoder.write_string(32767, name);
}
CommandNodeType::Argument { name, parser } => {
encoder.write_string(32767, name);
parser.encode(encoder);
}
}
if let Some(suggestion) = &self.suggestion {
encoder.write_string(32767, suggestion);
}
}
}
#[allow(unused)]
#[derive(Debug, Clone, Copy)]
pub enum Parser {
Bool,
Float { min: Option<f32>, max: Option<f32>, },
Double { min: Option<f64>, max: Option<f64>, },
Int { min: Option<i32>, max: Option<i32>, },
Long { min: Option<i64>, max: Option<i64>, },
String { kind: StringKind },
}
impl Parser {
pub fn encode(&self, encoder: &mut impl PacketEncoder) {
match self {
Self::Bool => encoder.write_varint(0),
Self::Float{ min, max } => {
encoder.write_varint(1);
encoder.write_byte( i8::from(min.is_some()) + 2 * i8::from(max.is_some()) );
if let Some(min) = min { encoder.write_float(*min) };
if let Some(max) = max { encoder.write_float(*max) };
},
Self::Double{ min, max } => {
encoder.write_varint(2);
encoder.write_byte( i8::from(min.is_some()) + 2 * i8::from(max.is_some()) );
if let Some(min) = min { encoder.write_double(*min) };
if let Some(max) = max { encoder.write_double(*max) };
},
Self::Int{ min, max } => {
encoder.write_varint(3);
encoder.write_byte( i8::from(min.is_some()) + 2 * i8::from(max.is_some()) );
if let Some(min) = min { encoder.write_int(*min) };
if let Some(max) = max { encoder.write_int(*max) };
},
Self::Long{ min, max } => {
encoder.write_varint(4);
encoder.write_byte( i8::from(min.is_some()) + 2 * i8::from(max.is_some()) );
if let Some(min) = min { encoder.write_long(*min) };
if let Some(max) = max { encoder.write_long(*max) };
},
Self::String{ kind } => {
encoder.write_varint(5);
encoder.write_varint(match kind {
StringKind::Single => 0,
StringKind::Quoted => 1,
StringKind::Greedy => 2,
})
},
}
}
}
#[allow(unused)]
#[derive(Debug, Clone, Copy)]
pub enum StringKind {
Single,
Quoted,
Greedy,
}

View File

@ -50,10 +50,11 @@ pub trait PacketEncoder: Write {
self.write_all(&data.to_be_bytes()).unwrap();
}
fn write_varint(&mut self, mut data: i32) {
fn write_varint(&mut self, data: i32) {
let mut data = data as u32;
loop {
let mut byte = (data & 0b11111111) as u8;
data = data >> 7;
data >>= 7;
if data != 0 {
byte |= 0b10000000;
}
@ -64,10 +65,11 @@ pub trait PacketEncoder: Write {
}
}
fn write_varlong(&mut self, mut data: i64) {
fn write_varlong(&mut self, data: i64) {
let mut data = data as u64;
loop {
let mut byte = (data & 0b11111111) as u8;
data = data >> 7;
data >>= 7;
if data != 0 {
byte |= 0b10000000;
}
@ -109,7 +111,7 @@ fn encode_varint(mut data: i32) -> Vec<u8> {
let mut res = Vec::new();
loop {
let mut byte = (data & 0b11111111) as u8;
data = data >> 7;
data >>= 7;
if data != 0 {
byte |= 0b10000000;
}
@ -136,8 +138,9 @@ pub struct PacketDecoder {
packet_id: i32,
}
#[allow(unused)]
impl PacketDecoder {
pub fn decode(read: &mut impl Read) -> Option<PacketDecoder> {
pub fn decode(read: &mut impl Read) -> Result<PacketDecoder, std::io::Error> {
let size = read_varint(read)? as usize;
let mut data = vec![0; size];
read.read_exact(&mut data).unwrap();
@ -147,11 +150,11 @@ impl PacketDecoder {
packet_id: 0
};
decoder.packet_id = decoder.read_varint();
return Some(decoder);
Ok(decoder)
}
pub fn packet_id(&self) -> i32 {
return self.packet_id;
self.packet_id
}
pub fn read_bytes(&mut self, n: usize) -> &[u8] {
@ -160,6 +163,12 @@ impl PacketDecoder {
ret
}
pub fn read_to_end(&mut self) -> &[u8] {
let ret = &self.data[self.idx..];
self.idx = self.data.len();
ret
}
pub fn read_bool(&mut self) -> bool {
self.idx += 1;
self.data[self.idx-1] != 0
@ -264,12 +273,12 @@ impl PacketDecoder {
}
}
fn read_varint(read: &mut impl Read) -> Option<i32> {
fn read_varint(read: &mut impl Read) -> Result<i32, std::io::Error> {
let mut result = 0;
let mut count = 0;
loop {
let mut byte = [0];
read.read_exact(&mut byte).ok()?;
read.read_exact(&mut byte)?;
let byte = byte[0];
result |= ((byte & 0x7f) as i32) << (7 * count);
count += 1;
@ -280,5 +289,5 @@ fn read_varint(read: &mut impl Read) -> Option<i32> {
break
}
}
Some(result)
Ok(result)
}

View File

@ -1,3 +1,5 @@
pub mod command;
pub mod data;
pub mod serverbound;
pub mod clientbound;
@ -12,7 +14,7 @@ pub enum NetworkState {
#[derive(Clone, Copy, Debug)]
pub struct Position {
x: i32,
y: i16,
z: i32
pub x: i32,
pub y: i16,
pub z: i32
}

View File

@ -30,31 +30,18 @@ pub struct SigData {
#[derive(Debug)]
pub struct LoginStart {
pub name: String,
pub sig_data: Option<SigData>,
pub uuid: Option<Uuid>,
pub uuid: Uuid,
}
impl LoginStart {
pub fn decode(mut decoder: PacketDecoder) -> Self {
let name = decoder.read_string();
let has_sig_data = decoder.read_bool();
let sig_data = if has_sig_data {
let timestamp = decoder.read_long();
let pubkey_len = decoder.read_varint();
let pubkey = decoder.read_bytes(pubkey_len as usize).to_vec();
let sig_len = decoder.read_varint();
let sig = decoder.read_bytes(sig_len as usize).to_vec();
Some(SigData { timestamp, pubkey, sig })
} else {
None
};
let has_uuid = decoder.read_bool();
let uuid = if has_uuid {
Some(decoder.read_uuid())
} else {
None
};
Self { name, sig_data, uuid }
if !has_uuid {
panic!("Client didn't supply UUID");
}
let uuid = decoder.read_uuid();
Self { name, uuid }
}
}
@ -73,6 +60,39 @@ impl ChatMessage {
}
}
#[derive(Debug)]
pub struct LoginPluginResponse {
pub id: i32,
pub data: Option<Vec<u8>>,
}
impl LoginPluginResponse {
pub fn decode(mut decoder: PacketDecoder) -> Self {
let id = decoder.read_varint();
let success = decoder.read_bool();
let data = if success {
Some(decoder.read_to_end().to_vec())
} else {
None
};
Self { id, data }
}
}
#[derive(Debug)]
pub struct SPluginMessage {
pub channel: String,
pub data: Vec<u8>,
}
impl SPluginMessage {
pub fn decode(mut decoder: PacketDecoder) -> Self {
let channel = decoder.read_string();
let data = decoder.read_to_end().to_vec();
Self { channel, data }
}
}
#[derive(Debug)]
pub enum ServerBoundPacket {
Unknown(i32),
@ -84,15 +104,18 @@ pub enum ServerBoundPacket {
PingRequest(i64),
// login
LoginStart(LoginStart),
LoginPluginResponse(LoginPluginResponse),
// play
ChatMessage(ChatMessage),
ChatCommand(ChatMessage),
PluginMessage(SPluginMessage),
}
impl ServerBoundPacket {
pub fn decode(state: &mut NetworkState, mut decoder: PacketDecoder) -> ServerBoundPacket {
use NetworkState as NS;
match (*state, decoder.packet_id()) {
(NS::Handshake, 0) => {
(NS::Handshake, 0x00) => {
let hs = Handshake::decode(decoder);
match hs.next_state {
1 => *state = NS::Status,
@ -101,16 +124,24 @@ impl ServerBoundPacket {
}
ServerBoundPacket::Handshake(hs)
},
(NS::Status, 0)
(NS::Status, 0x00)
=> ServerBoundPacket::StatusRequest(),
(NS::Status, 1)
(NS::Status, 0x01)
=> ServerBoundPacket::PingRequest(decoder.read_long()),
(NS::Login, 0) => {
*state = NS::Play;
(NS::Login, 0x00) => {
ServerBoundPacket::LoginStart(LoginStart::decode(decoder))
},
(NS::Play, 5) => ServerBoundPacket::ChatMessage(ChatMessage::decode(decoder)),
(NS::Play, id @ (18 | 20 | 21 | 22 | 30)) => ServerBoundPacket::Ignored(id),
(NS::Login, 0x02) => {
let lpr = LoginPluginResponse::decode(decoder);
if lpr.id == -1 {
*state = NetworkState::Play;
}
ServerBoundPacket::LoginPluginResponse(lpr)
},
(NS::Play, 0x04) => ServerBoundPacket::ChatCommand(ChatMessage::decode(decoder)),
(NS::Play, 0x05) => ServerBoundPacket::ChatMessage(ChatMessage::decode(decoder)),
(NS::Play, 0x0C) => ServerBoundPacket::PluginMessage(SPluginMessage::decode(decoder)),
(NS::Play, id @ (0x11 | 0x13 | 0x14 | 0x15 | 0x1d)) => ServerBoundPacket::Ignored(id),
(_, id) => ServerBoundPacket::Unknown(id),
}
}

Binary file not shown.