mirror of
https://github.com/mat-1/azalea.git
synced 2025-08-02 23:44:38 +00:00
Merge branch 'main' into pathfinding
This commit is contained in:
commit
2d3031517f
76 changed files with 2123 additions and 422 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -3,3 +3,7 @@
|
|||
flamegraph.svg
|
||||
perf.data
|
||||
perf.data.old
|
||||
|
||||
# created by azalea-auth/examples/auth, defined in the main .gitignore because
|
||||
# the example could be run from anywhere
|
||||
example_cache.json
|
||||
|
|
492
Cargo.lock
generated
492
Cargo.lock
generated
|
@ -100,7 +100,7 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
|
|||
|
||||
[[package]]
|
||||
name = "azalea"
|
||||
version = "0.1.0"
|
||||
version = "0.2.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
|
@ -114,15 +114,25 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "azalea-auth"
|
||||
version = "0.1.0"
|
||||
version = "0.2.2"
|
||||
dependencies = [
|
||||
"azalea-buf",
|
||||
"azalea-crypto",
|
||||
"chrono",
|
||||
"env_logger",
|
||||
"log",
|
||||
"num-bigint",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "azalea-block"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"azalea-block-macros",
|
||||
"azalea-buf",
|
||||
|
@ -130,7 +140,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "azalea-block-macros"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
@ -139,11 +149,11 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "azalea-brigadier"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
|
||||
[[package]]
|
||||
name = "azalea-buf"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"azalea-buf-macros",
|
||||
"byteorder",
|
||||
|
@ -155,7 +165,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "azalea-buf-macros"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
@ -164,7 +174,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "azalea-chat"
|
||||
version = "0.1.1"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"azalea-buf",
|
||||
"azalea-language",
|
||||
|
@ -175,7 +185,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "azalea-client"
|
||||
version = "0.1.0"
|
||||
version = "0.2.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"azalea-auth",
|
||||
|
@ -195,7 +205,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "azalea-core"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"azalea-buf",
|
||||
"azalea-chat",
|
||||
|
@ -205,7 +215,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "azalea-crypto"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"azalea-buf",
|
||||
|
@ -220,7 +230,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "azalea-language"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"serde",
|
||||
|
@ -229,7 +239,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "azalea-nbt"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"azalea-buf",
|
||||
|
@ -258,7 +268,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "azalea-physics"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"azalea-block",
|
||||
"azalea-core",
|
||||
|
@ -269,7 +279,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "azalea-protocol"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"async-compression",
|
||||
"async-recursion",
|
||||
|
@ -301,7 +311,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "azalea-protocol-macros"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
@ -310,7 +320,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "azalea-registry"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"azalea-buf",
|
||||
"azalea-registry-macros",
|
||||
|
@ -318,7 +328,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "azalea-registry-macros"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
@ -327,7 +337,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "azalea-world"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"azalea-block",
|
||||
"azalea-buf",
|
||||
|
@ -341,6 +351,12 @@ dependencies = [
|
|||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
|
@ -358,7 +374,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "bot"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"azalea",
|
||||
|
@ -407,6 +423,12 @@ dependencies = [
|
|||
"rustc_version",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.0.73"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11"
|
||||
|
||||
[[package]]
|
||||
name = "cfb8"
|
||||
version = "0.8.1"
|
||||
|
@ -424,9 +446,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
|||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.19"
|
||||
version = "0.4.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73"
|
||||
checksum = "bfd4d1b31faaa3a89d7934dbded3111da0d2ef28e3ebccdb4f0179f5929d1ef1"
|
||||
dependencies = [
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
|
@ -453,6 +475,22 @@ dependencies = [
|
|||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.9.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[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.2"
|
||||
|
@ -606,6 +644,15 @@ version = "1.6.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
|
||||
|
||||
[[package]]
|
||||
name = "encoding_rs"
|
||||
version = "0.8.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9852635589dc9f9ea1b6fe9f05b50ef208c85c834a562f0c6abb1c475736ec2b"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "enum-as-inner"
|
||||
version = "0.3.4"
|
||||
|
@ -631,6 +678,15 @@ dependencies = [
|
|||
"termcolor",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "1.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499"
|
||||
dependencies = [
|
||||
"instant",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.0.24"
|
||||
|
@ -641,6 +697,27 @@ dependencies = [
|
|||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fnv"
|
||||
version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
|
||||
dependencies = [
|
||||
"foreign-types-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types-shared"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
|
||||
|
||||
[[package]]
|
||||
name = "form_urlencoded"
|
||||
version = "1.0.1"
|
||||
|
@ -758,7 +835,26 @@ checksum = "9be70c98951c83b8d2f8f60d7065fa6d5146873094452a1008da8c2f1e4205ad"
|
|||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"wasi 0.10.2+wasi-snapshot-preview1",
|
||||
"wasi 0.10.0+wasi-snapshot-preview1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
version = "0.3.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ca32592cf21ac7ccab1825cd87f6c9b3d9022c44d086172ed0966bec8af30be"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"fnv",
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"futures-util",
|
||||
"http",
|
||||
"indexmap",
|
||||
"slab",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -799,12 +895,83 @@ dependencies = [
|
|||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "0.2.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"fnv",
|
||||
"itoa 1.0.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http-body"
|
||||
version = "0.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"http",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httparse"
|
||||
version = "1.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904"
|
||||
|
||||
[[package]]
|
||||
name = "httpdate"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421"
|
||||
|
||||
[[package]]
|
||||
name = "humantime"
|
||||
version = "2.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "0.14.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02c929dc5c39e335a03c405292728118860721b10190d98c2a0f0efd5baafbac"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"h2",
|
||||
"http",
|
||||
"http-body",
|
||||
"httparse",
|
||||
"httpdate",
|
||||
"itoa 1.0.2",
|
||||
"pin-project-lite",
|
||||
"socket2 0.4.4",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
"want",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-tls"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"hyper",
|
||||
"native-tls",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "0.2.3"
|
||||
|
@ -853,7 +1020,7 @@ dependencies = [
|
|||
"socket2 0.3.19",
|
||||
"widestring",
|
||||
"winapi",
|
||||
"winreg",
|
||||
"winreg 0.6.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -965,6 +1132,12 @@ dependencies = [
|
|||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mime"
|
||||
version = "0.3.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.5.3"
|
||||
|
@ -986,6 +1159,24 @@ dependencies = [
|
|||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "native-tls"
|
||||
version = "0.2.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fd7e2f3618557f980e0b17e8856252eee3c97fa12c54dff0ca290fb6266ca4a9"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"libc",
|
||||
"log",
|
||||
"openssl",
|
||||
"openssl-probe",
|
||||
"openssl-sys",
|
||||
"schannel",
|
||||
"security-framework",
|
||||
"security-framework-sys",
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nohash-hasher"
|
||||
version = "0.2.0"
|
||||
|
@ -1091,9 +1282,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.12.0"
|
||||
version = "1.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7709cef83f0c1f58f666e746a08b21e0085f7440fa6a29cc194d68aac97a4225"
|
||||
checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1"
|
||||
|
||||
[[package]]
|
||||
name = "oorandom"
|
||||
|
@ -1101,6 +1292,51 @@ version = "11.1.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575"
|
||||
|
||||
[[package]]
|
||||
name = "openssl"
|
||||
version = "0.10.42"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "12fc0523e3bd51a692c8850d075d74dc062ccf251c0110668cbd921917118a13"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cfg-if",
|
||||
"foreign-types",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"openssl-macros",
|
||||
"openssl-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-macros"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-probe"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
|
||||
|
||||
[[package]]
|
||||
name = "openssl-sys"
|
||||
version = "0.9.76"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5230151e44c0f05157effb743e8d517472843121cf9243e8b81393edb5acd9ce"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"cc",
|
||||
"libc",
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.11.2"
|
||||
|
@ -1167,6 +1403,12 @@ version = "0.1.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
||||
|
||||
[[package]]
|
||||
name = "pkg-config"
|
||||
version = "0.3.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae"
|
||||
|
||||
[[package]]
|
||||
name = "plotters"
|
||||
version = "0.3.1"
|
||||
|
@ -1321,6 +1563,52 @@ version = "0.6.26"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49b3de9ec5dc0a3417da371aab17d729997c15010e7fd24ff707773a33bddb64"
|
||||
|
||||
[[package]]
|
||||
name = "remove_dir_all"
|
||||
version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.11.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "431949c384f4e2ae07605ccaa56d1d9d2ecdb5cadd4f9577ccfab29f2e5149fc"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"bytes",
|
||||
"encoding_rs",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"h2",
|
||||
"http",
|
||||
"http-body",
|
||||
"hyper",
|
||||
"hyper-tls",
|
||||
"ipnet",
|
||||
"js-sys",
|
||||
"log",
|
||||
"mime",
|
||||
"native-tls",
|
||||
"once_cell",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
"tower-service",
|
||||
"url",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
"winreg 0.10.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "resolv-conf"
|
||||
version = "0.7.0"
|
||||
|
@ -1366,12 +1654,45 @@ dependencies = [
|
|||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "schannel"
|
||||
version = "0.1.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "88d6731146462ea25d9244b2ed5fd1d716d25c52e4d54aa4fb0f3c4e9854dbe2"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
|
||||
|
||||
[[package]]
|
||||
name = "security-framework"
|
||||
version = "2.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2bc1bb97804af6631813c55739f771071e0f2ed33ee20b68c86ec505d906356c"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"core-foundation",
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
"security-framework-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "security-framework-sys"
|
||||
version = "2.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.10"
|
||||
|
@ -1380,9 +1701,9 @@ checksum = "a41d061efea015927ac527063765e73601444cdc344ba855bc7bd44578b25e1c"
|
|||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.137"
|
||||
version = "1.0.145"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "61ea8d54c77f8315140a05f4c7237403bf38b72704d031543aa1d16abbf517d1"
|
||||
checksum = "728eb6351430bccb993660dfffc5a72f91ccc1295abaa8ce19b27ebe4f75568b"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
@ -1399,9 +1720,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.137"
|
||||
version = "1.0.145"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f26faba0c3959972377d3b2d306ee9f71faee9714294e41bb777f83f88578be"
|
||||
checksum = "81fa1584d3d1bcacd84c277a0dfe21f5b0f6accf4a23d04d4c6d61f1af522b4c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
@ -1410,15 +1731,27 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.81"
|
||||
version = "1.0.86"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b7ce2b32a1aed03c558dc61a5cd328f15aff2dbc17daad8fb8af04d2100e15c"
|
||||
checksum = "41feea4228a6f1cd09ec7a3593a682276702cd67b5273544757dae23c096f074"
|
||||
dependencies = [
|
||||
"itoa 1.0.2",
|
||||
"ryu",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_urlencoded"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
|
||||
dependencies = [
|
||||
"form_urlencoded",
|
||||
"itoa 1.0.2",
|
||||
"ryu",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha-1"
|
||||
version = "0.10.0"
|
||||
|
@ -1430,6 +1763,15 @@ dependencies = [
|
|||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-registry"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "simple_asn1"
|
||||
version = "0.5.4"
|
||||
|
@ -1486,6 +1828,20 @@ dependencies = [
|
|||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"fastrand",
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
"remove_dir_all",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "termcolor"
|
||||
version = "1.1.3"
|
||||
|
@ -1551,9 +1907,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
|
|||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.21.1"
|
||||
version = "1.21.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0020c875007ad96677dcc890298f4b942882c5d4eb7cc8f439fc3bf813dc9c95"
|
||||
checksum = "a9e03c497dc955702ba729190dc4aac6f2a0ce97f913e5b1b5912fc5039d9099"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"bytes",
|
||||
|
@ -1561,8 +1917,9 @@ dependencies = [
|
|||
"memchr",
|
||||
"mio",
|
||||
"num_cpus",
|
||||
"once_cell",
|
||||
"parking_lot 0.12.1",
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
"socket2 0.4.4",
|
||||
"tokio-macros",
|
||||
"winapi",
|
||||
|
@ -1579,6 +1936,16 @@ dependencies = [
|
|||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-native-tls"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b"
|
||||
dependencies = [
|
||||
"native-tls",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-util"
|
||||
version = "0.7.4"
|
||||
|
@ -1593,6 +1960,12 @@ dependencies = [
|
|||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower-service"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52"
|
||||
|
||||
[[package]]
|
||||
name = "tracing"
|
||||
version = "0.1.35"
|
||||
|
@ -1658,6 +2031,12 @@ dependencies = [
|
|||
"trust-dns-proto",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "try-lock"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642"
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.15.0"
|
||||
|
@ -1709,6 +2088,12 @@ version = "1.1.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd6469f4314d5f1ffec476e05f17cc9a78bc7a27a6a857842170bdf8d6f98d2f"
|
||||
|
||||
[[package]]
|
||||
name = "vcpkg"
|
||||
version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.4"
|
||||
|
@ -1727,10 +2112,20 @@ dependencies = [
|
|||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.10.2+wasi-snapshot-preview1"
|
||||
name = "want"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
|
||||
checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0"
|
||||
dependencies = [
|
||||
"log",
|
||||
"try-lock",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.10.0+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
|
@ -1763,6 +2158,18 @@ dependencies = [
|
|||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-futures"
|
||||
version = "0.4.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6f741de44b75e14c35df886aff5f1eb73aa114fa5d4d00dcd37b5e01259bf3b2"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.80"
|
||||
|
@ -1890,3 +2297,12 @@ checksum = "b2986deb581c4fe11b621998a5e53361efe6b48a151178d0cd9eeffa4dc6acc9"
|
|||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winreg"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# Azalea
|
||||
|
||||
A collection of Rust crates primarily for creating Minecraft bots.
|
||||
A collection of Rust crates for making Minecraft bots, clients, and tools.
|
||||
|
||||
<p align="center">
|
||||
<img src="https://cdn.matdoes.dev/images/flowering_azalea.webp" alt="Azalea" height="200">
|
||||
|
|
19
azalea-auth/Cargo.toml
Executable file → Normal file
19
azalea-auth/Cargo.toml
Executable file → Normal file
|
@ -1,12 +1,25 @@
|
|||
[package]
|
||||
description = "A port of Mojang's Authlib, except authentication isn't actually implemented yet."
|
||||
description = "A port of Mojang's Authlib and launcher authentication."
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
name = "azalea-auth"
|
||||
version = "0.1.0"
|
||||
version = "0.2.2"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
azalea-buf = {path = "../azalea-buf", version = "^0.1.0"}
|
||||
azalea-buf = { path = "../azalea-buf", version = "^0.2.0" }
|
||||
azalea-crypto = { path = "../azalea-crypto", version = "^0.2.0" }
|
||||
chrono = { version = "0.4.22", default-features = false }
|
||||
log = "0.4.17"
|
||||
num-bigint = "0.4.3"
|
||||
reqwest = { version = "0.11.12", features = ["json"] }
|
||||
serde = { version = "1.0.145", features = ["derive"] }
|
||||
serde_json = "1.0.86"
|
||||
thiserror = "1.0.37"
|
||||
tokio = { version = "1.21.2", features = ["fs"] }
|
||||
uuid = "^1.1.2"
|
||||
|
||||
[dev-dependencies]
|
||||
env_logger = "0.9.1"
|
||||
tokio = { version = "1.21.2", features = ["full"] }
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
# Azalea Auth
|
||||
|
||||
A port of Mojang's Authlib, except authentication isn't actually implemented yet.
|
||||
A port of Mojang's Authlib and launcher authentication.
|
||||
|
||||
Thanks to [wiki.vg contributors](https://wiki.vg/Microsoft_Authentication_Scheme), [Overhash](https://gist.github.com/OverHash/a71b32846612ba09d8f79c9d775bfadf), and [prismarine-auth contributors](https://github.com/PrismarineJS/prismarine-auth).
|
||||
|
|
19
azalea-auth/examples/auth.rs
Normal file
19
azalea-auth/examples/auth.rs
Normal file
|
@ -0,0 +1,19 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
env_logger::init();
|
||||
|
||||
let cache_file = PathBuf::from("example_cache.json");
|
||||
|
||||
let auth_result = azalea_auth::auth(
|
||||
"example@example.com",
|
||||
azalea_auth::AuthOpts {
|
||||
cache_file: Some(cache_file),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
println!("{:?}", auth_result);
|
||||
}
|
486
azalea-auth/src/auth.rs
Normal file
486
azalea-auth/src/auth.rs
Normal file
|
@ -0,0 +1,486 @@
|
|||
//! Handle Minecraft (Xbox) authentication.
|
||||
|
||||
use crate::cache::{self, CachedAccount, ExpiringValue};
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
path::PathBuf,
|
||||
time::{Instant, SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct AuthOpts {
|
||||
/// Whether we should check if the user actually owns the game. This will
|
||||
/// fail if the user has Xbox Game Pass! Note that this isn't really
|
||||
/// necessary, since getting the user profile will check this anyways.
|
||||
pub check_ownership: bool,
|
||||
// /// Whether we should get the Minecraft profile data (i.e. username, uuid,
|
||||
// /// skin, etc) for the player.
|
||||
// pub get_profile: bool,
|
||||
/// The directory to store the cache in. If this is not set, caching is not
|
||||
/// done.
|
||||
pub cache_file: Option<PathBuf>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum AuthError {
|
||||
#[error(
|
||||
"The Minecraft API is indicating that you don't own the game. \
|
||||
If you're using Xbox Game Pass, set `check_ownership` to false in the auth options."
|
||||
)]
|
||||
DoesNotOwnGame,
|
||||
#[error("Error getting Microsoft auth token: {0}")]
|
||||
GetMicrosoftAuthToken(#[from] GetMicrosoftAuthTokenError),
|
||||
#[error("Error refreshing Microsoft auth token: {0}")]
|
||||
RefreshMicrosoftAuthToken(#[from] RefreshMicrosoftAuthTokenError),
|
||||
#[error("Error getting Xbox Live auth token: {0}")]
|
||||
GetXboxLiveAuthToken(#[from] MinecraftXstsAuthError),
|
||||
#[error("Error getting Minecraft profile: {0}")]
|
||||
GetMinecraftProfile(#[from] GetProfileError),
|
||||
#[error("Error checking ownership: {0}")]
|
||||
CheckOwnership(#[from] CheckOwnershipError),
|
||||
#[error("Error getting Minecraft auth token: {0}")]
|
||||
GetMinecraftAuthToken(#[from] MinecraftAuthError),
|
||||
#[error("Error authenticating with Xbox Live: {0}")]
|
||||
GetXboxLiveAuth(#[from] XboxLiveAuthError),
|
||||
}
|
||||
|
||||
/// Authenticate with Microsoft. If the data isn't cached,
|
||||
/// they'll be asked to go to log into Microsoft in a web page.
|
||||
///
|
||||
/// The email is technically only used as a cache key, so it *could* be
|
||||
/// anything. You should just have it be the actual email so it's not confusing
|
||||
/// though, and in case the Microsoft API does start providing the real email.
|
||||
pub async fn auth(email: &str, opts: AuthOpts) -> Result<AuthResult, AuthError> {
|
||||
let cached_account = if let Some(cache_file) = &opts.cache_file {
|
||||
cache::get_account_in_cache(cache_file, email).await
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// these two MUST be set by the end, since we return them in AuthResult
|
||||
let profile: ProfileResponse;
|
||||
let minecraft_access_token: String;
|
||||
|
||||
if cached_account.is_some() && !cached_account.as_ref().unwrap().mca.is_expired() {
|
||||
let account = cached_account.as_ref().unwrap();
|
||||
// the minecraft auth data is cached and not expired, so we can just
|
||||
// use that instead of doing auth all over again :)
|
||||
profile = account.profile.clone();
|
||||
minecraft_access_token = account.mca.data.access_token.clone();
|
||||
} else {
|
||||
let client = reqwest::Client::new();
|
||||
let mut msa = if let Some(account) = cached_account {
|
||||
account.msa
|
||||
} else {
|
||||
interactive_get_ms_auth_token(&client).await?
|
||||
};
|
||||
if msa.is_expired() {
|
||||
log::trace!("refreshing Microsoft auth token");
|
||||
msa = refresh_ms_auth_token(&client, &msa.data.refresh_token).await?;
|
||||
}
|
||||
let ms_access_token = &msa.data.access_token;
|
||||
log::trace!("Got access token: {}", ms_access_token);
|
||||
|
||||
let xbl_auth = auth_with_xbox_live(&client, ms_access_token).await?;
|
||||
|
||||
let xsts_token = obtain_xsts_for_minecraft(
|
||||
&client,
|
||||
&xbl_auth
|
||||
.get()
|
||||
.expect("Xbox Live auth token shouldn't have expired yet")
|
||||
.token,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Minecraft auth
|
||||
let mca = auth_with_minecraft(&client, &xbl_auth.data.user_hash, &xsts_token).await?;
|
||||
|
||||
minecraft_access_token = mca
|
||||
.get()
|
||||
.expect("Minecraft auth shouldn't have expired yet")
|
||||
.access_token
|
||||
.to_string();
|
||||
|
||||
if opts.check_ownership {
|
||||
let has_game = check_ownership(&client, &minecraft_access_token).await?;
|
||||
if !has_game {
|
||||
return Err(AuthError::DoesNotOwnGame);
|
||||
}
|
||||
}
|
||||
|
||||
profile = get_profile(&client, &minecraft_access_token).await?;
|
||||
|
||||
if let Some(cache_file) = opts.cache_file {
|
||||
if let Err(e) = cache::set_account_in_cache(
|
||||
&cache_file,
|
||||
email,
|
||||
CachedAccount {
|
||||
email: email.to_string(),
|
||||
mca,
|
||||
msa,
|
||||
xbl: xbl_auth,
|
||||
profile: profile.clone(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
log::error!("{}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(AuthResult {
|
||||
access_token: minecraft_access_token,
|
||||
profile,
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct AuthResult {
|
||||
pub access_token: String,
|
||||
pub profile: ProfileResponse,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct DeviceCodeResponse {
|
||||
user_code: String,
|
||||
device_code: String,
|
||||
verification_uri: String,
|
||||
expires_in: u64,
|
||||
interval: u64,
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct AccessTokenResponse {
|
||||
token_type: String,
|
||||
expires_in: u64,
|
||||
scope: String,
|
||||
access_token: String,
|
||||
refresh_token: String,
|
||||
user_id: String,
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
pub struct XboxLiveAuthResponse {
|
||||
issue_instant: String,
|
||||
not_after: String,
|
||||
token: String,
|
||||
display_claims: HashMap<String, Vec<HashMap<String, String>>>,
|
||||
}
|
||||
|
||||
/// Just the important data
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct XboxLiveAuth {
|
||||
token: String,
|
||||
user_hash: String,
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct MinecraftAuthResponse {
|
||||
username: String,
|
||||
roles: Vec<String>,
|
||||
access_token: String,
|
||||
token_type: String,
|
||||
expires_in: u64,
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct GameOwnershipResponse {
|
||||
items: Vec<GameOwnershipItem>,
|
||||
signature: String,
|
||||
key_id: String,
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct GameOwnershipItem {
|
||||
name: String,
|
||||
signature: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct ProfileResponse {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub skins: Vec<serde_json::Value>,
|
||||
pub capes: Vec<serde_json::Value>,
|
||||
}
|
||||
|
||||
// nintendo switch (so it works for accounts that are under 18 years old)
|
||||
const CLIENT_ID: &str = "00000000441cc96b";
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum GetMicrosoftAuthTokenError {
|
||||
#[error("Http error: {0}")]
|
||||
Http(#[from] reqwest::Error),
|
||||
#[error("Authentication timed out")]
|
||||
Timeout,
|
||||
}
|
||||
|
||||
/// Asks the user to go to a webpage and log in with Microsoft.
|
||||
async fn interactive_get_ms_auth_token(
|
||||
client: &reqwest::Client,
|
||||
) -> Result<ExpiringValue<AccessTokenResponse>, GetMicrosoftAuthTokenError> {
|
||||
let res = client
|
||||
.post("https://login.live.com/oauth20_connect.srf")
|
||||
.form(&vec![
|
||||
("scope", "service::user.auth.xboxlive.com::MBI_SSL"),
|
||||
("client_id", CLIENT_ID),
|
||||
("response_type", "device_code"),
|
||||
])
|
||||
.send()
|
||||
.await?
|
||||
.json::<DeviceCodeResponse>()
|
||||
.await?;
|
||||
log::trace!("Device code response: {:?}", res);
|
||||
println!(
|
||||
"Go to \x1b[1m{}\x1b[m and enter the code \x1b[1m{}\x1b[m",
|
||||
res.verification_uri, res.user_code
|
||||
);
|
||||
|
||||
let login_expires_at = Instant::now() + std::time::Duration::from_secs(res.expires_in);
|
||||
|
||||
while Instant::now() < login_expires_at {
|
||||
tokio::time::sleep(std::time::Duration::from_secs(res.interval)).await;
|
||||
|
||||
log::trace!("Polling to check if user has logged in...");
|
||||
if let Ok(access_token_response) = client
|
||||
.post(format!(
|
||||
"https://login.live.com/oauth20_token.srf?client_id={}",
|
||||
CLIENT_ID
|
||||
))
|
||||
.form(&vec![
|
||||
("client_id", CLIENT_ID),
|
||||
("device_code", &res.device_code),
|
||||
("grant_type", "urn:ietf:params:oauth:grant-type:device_code"),
|
||||
])
|
||||
.send()
|
||||
.await?
|
||||
.json::<AccessTokenResponse>()
|
||||
.await
|
||||
{
|
||||
log::trace!("access_token_response: {:?}", access_token_response);
|
||||
let expires_at = SystemTime::now()
|
||||
+ std::time::Duration::from_secs(access_token_response.expires_in);
|
||||
return Ok(ExpiringValue {
|
||||
data: access_token_response,
|
||||
expires_at: expires_at
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.expect("Time went backwards")
|
||||
.as_secs(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Err(GetMicrosoftAuthTokenError::Timeout)
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum RefreshMicrosoftAuthTokenError {
|
||||
#[error("Http error: {0}")]
|
||||
Http(#[from] reqwest::Error),
|
||||
}
|
||||
|
||||
async fn refresh_ms_auth_token(
|
||||
client: &reqwest::Client,
|
||||
refresh_token: &str,
|
||||
) -> Result<ExpiringValue<AccessTokenResponse>, RefreshMicrosoftAuthTokenError> {
|
||||
let access_token_response = client
|
||||
.post("https://login.live.com/oauth20_token.srf")
|
||||
.form(&vec![
|
||||
("scope", "service::user.auth.xboxlive.com::MBI_SSL"),
|
||||
("client_id", CLIENT_ID),
|
||||
("grant_type", "refresh_token"),
|
||||
("refresh_token", refresh_token),
|
||||
])
|
||||
.send()
|
||||
.await?
|
||||
.json::<AccessTokenResponse>()
|
||||
.await?;
|
||||
|
||||
let expires_at =
|
||||
SystemTime::now() + std::time::Duration::from_secs(access_token_response.expires_in);
|
||||
Ok(ExpiringValue {
|
||||
data: access_token_response,
|
||||
expires_at: expires_at
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.expect("Time went backwards")
|
||||
.as_secs(),
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum XboxLiveAuthError {
|
||||
#[error("Http error: {0}")]
|
||||
Http(#[from] reqwest::Error),
|
||||
#[error("Invalid expiry date: {0}")]
|
||||
InvalidExpiryDate(String),
|
||||
}
|
||||
|
||||
async fn auth_with_xbox_live(
|
||||
client: &reqwest::Client,
|
||||
access_token: &str,
|
||||
) -> Result<ExpiringValue<XboxLiveAuth>, XboxLiveAuthError> {
|
||||
let auth_json = json!({
|
||||
"Properties": {
|
||||
"AuthMethod": "RPS",
|
||||
"SiteName": "user.auth.xboxlive.com",
|
||||
// i thought this was supposed to be d={} but it doesn't work for
|
||||
// me when i add it ??????
|
||||
"RpsTicket": format!("{}", access_token)
|
||||
},
|
||||
"RelyingParty": "http://auth.xboxlive.com",
|
||||
"TokenType": "JWT"
|
||||
});
|
||||
let payload = auth_json.to_string();
|
||||
log::trace!("auth_json: {:#?}", auth_json);
|
||||
let res = client
|
||||
.post("https://user.auth.xboxlive.com/user/authenticate")
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Accept", "application/json")
|
||||
.header("x-xbl-contract-version", "1")
|
||||
// .header("Cache-Control", "no-store, must-revalidate, no-cache")
|
||||
// .header("Signature", base64::encode(signature))
|
||||
.body(payload)
|
||||
.send()
|
||||
.await?
|
||||
.json::<XboxLiveAuthResponse>()
|
||||
.await?;
|
||||
log::trace!("Xbox Live auth response: {:?}", res);
|
||||
|
||||
// not_after looks like 2020-12-21T19:52:08.4463796Z
|
||||
let expires_at = DateTime::parse_from_rfc3339(&res.not_after)
|
||||
.map_err(|e| XboxLiveAuthError::InvalidExpiryDate(format!("{}: {}", res.not_after, e)))?
|
||||
.with_timezone(&Utc)
|
||||
.timestamp() as u64;
|
||||
Ok(ExpiringValue {
|
||||
data: XboxLiveAuth {
|
||||
token: res.token,
|
||||
user_hash: res.display_claims["xui"].get(0).unwrap()["uhs"].clone(),
|
||||
},
|
||||
expires_at,
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum MinecraftXstsAuthError {
|
||||
#[error("Http error: {0}")]
|
||||
Http(#[from] reqwest::Error),
|
||||
}
|
||||
|
||||
async fn obtain_xsts_for_minecraft(
|
||||
client: &reqwest::Client,
|
||||
xbl_auth_token: &str,
|
||||
) -> Result<String, MinecraftXstsAuthError> {
|
||||
let res = client
|
||||
.post("https://xsts.auth.xboxlive.com/xsts/authorize")
|
||||
.header("Accept", "application/json")
|
||||
.json(&json!({
|
||||
"Properties": {
|
||||
"SandboxId": "RETAIL",
|
||||
"UserTokens": [xbl_auth_token.to_string()]
|
||||
},
|
||||
"RelyingParty": "rp://api.minecraftservices.com/",
|
||||
"TokenType": "JWT"
|
||||
}))
|
||||
.send()
|
||||
.await?
|
||||
.json::<XboxLiveAuthResponse>()
|
||||
.await?;
|
||||
log::trace!("Xbox Live auth response (for XSTS): {:?}", res);
|
||||
|
||||
Ok(res.token)
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum MinecraftAuthError {
|
||||
#[error("Http error: {0}")]
|
||||
Http(#[from] reqwest::Error),
|
||||
}
|
||||
|
||||
async fn auth_with_minecraft(
|
||||
client: &reqwest::Client,
|
||||
user_hash: &str,
|
||||
xsts_token: &str,
|
||||
) -> Result<ExpiringValue<MinecraftAuthResponse>, MinecraftAuthError> {
|
||||
let res = client
|
||||
.post("https://api.minecraftservices.com/authentication/login_with_xbox")
|
||||
.header("Accept", "application/json")
|
||||
.json(&json!({
|
||||
"identityToken": format!("XBL3.0 x={};{}", user_hash, xsts_token)
|
||||
}))
|
||||
.send()
|
||||
.await?
|
||||
.json::<MinecraftAuthResponse>()
|
||||
.await?;
|
||||
log::trace!("{:?}", res);
|
||||
|
||||
let expires_at = SystemTime::now() + std::time::Duration::from_secs(res.expires_in);
|
||||
Ok(ExpiringValue {
|
||||
data: res,
|
||||
// to seconds since epoch
|
||||
expires_at: expires_at.duration_since(UNIX_EPOCH).unwrap().as_secs(),
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum CheckOwnershipError {
|
||||
#[error("Http error: {0}")]
|
||||
Http(#[from] reqwest::Error),
|
||||
}
|
||||
|
||||
async fn check_ownership(
|
||||
client: &reqwest::Client,
|
||||
minecraft_access_token: &str,
|
||||
) -> Result<bool, CheckOwnershipError> {
|
||||
let res = client
|
||||
.get("https://api.minecraftservices.com/entitlements/mcstore")
|
||||
.header(
|
||||
"Authorization",
|
||||
format!("Bearer {}", minecraft_access_token),
|
||||
)
|
||||
.send()
|
||||
.await?
|
||||
.json::<GameOwnershipResponse>()
|
||||
.await?;
|
||||
log::trace!("{:?}", res);
|
||||
|
||||
// vanilla checks here to make sure the signatures are right, but it's not
|
||||
// actually required so we just don't
|
||||
|
||||
Ok(!res.items.is_empty())
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum GetProfileError {
|
||||
#[error("Http error: {0}")]
|
||||
Http(#[from] reqwest::Error),
|
||||
}
|
||||
|
||||
async fn get_profile(
|
||||
client: &reqwest::Client,
|
||||
minecraft_access_token: &str,
|
||||
) -> Result<ProfileResponse, GetProfileError> {
|
||||
let res = client
|
||||
.get("https://api.minecraftservices.com/minecraft/profile")
|
||||
.header(
|
||||
"Authorization",
|
||||
format!("Bearer {}", minecraft_access_token),
|
||||
)
|
||||
.send()
|
||||
.await?
|
||||
.json::<ProfileResponse>()
|
||||
.await?;
|
||||
log::trace!("{:?}", res);
|
||||
|
||||
Ok(res)
|
||||
}
|
117
azalea-auth/src/cache.rs
Normal file
117
azalea-auth/src/cache.rs
Normal file
|
@ -0,0 +1,117 @@
|
|||
//! Cache auth information
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::Path;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use thiserror::Error;
|
||||
use tokio::fs::File;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum CacheError {
|
||||
#[error("Failed to read cache file: {0}")]
|
||||
Read(std::io::Error),
|
||||
#[error("Failed to write cache file: {0}")]
|
||||
Write(std::io::Error),
|
||||
#[error("Failed to create cache file directory: {0}")]
|
||||
MkDir(std::io::Error),
|
||||
#[error("Failed to parse cache file: {0}")]
|
||||
Parse(serde_json::Error),
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
pub struct CachedAccount {
|
||||
pub email: String,
|
||||
/// Microsoft auth
|
||||
pub msa: ExpiringValue<crate::auth::AccessTokenResponse>,
|
||||
/// Xbox Live auth
|
||||
pub xbl: ExpiringValue<crate::auth::XboxLiveAuth>,
|
||||
/// Minecraft auth
|
||||
pub mca: ExpiringValue<crate::auth::MinecraftAuthResponse>,
|
||||
/// The user's Minecraft profile (i.e. username, UUID, skin)
|
||||
pub profile: crate::auth::ProfileResponse,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
pub struct ExpiringValue<T> {
|
||||
/// Seconds since the UNIX epoch
|
||||
pub expires_at: u64,
|
||||
pub data: T,
|
||||
}
|
||||
|
||||
impl<T> ExpiringValue<T> {
|
||||
pub fn is_expired(&self) -> bool {
|
||||
self.expires_at
|
||||
< SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs()
|
||||
}
|
||||
|
||||
/// Return the data if it's not expired, otherwise return `None`
|
||||
pub fn get(&self) -> Option<&T> {
|
||||
if self.is_expired() {
|
||||
None
|
||||
} else {
|
||||
Some(&self.data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_entire_cache(cache_file: &Path) -> Result<Vec<CachedAccount>, CacheError> {
|
||||
let mut cache: Vec<CachedAccount> = Vec::new();
|
||||
if cache_file.exists() {
|
||||
let mut cache_file = File::open(cache_file).await.map_err(CacheError::Read)?;
|
||||
// read the file into a string
|
||||
let mut contents = String::new();
|
||||
cache_file
|
||||
.read_to_string(&mut contents)
|
||||
.await
|
||||
.map_err(CacheError::Read)?;
|
||||
cache = serde_json::from_str(&contents).map_err(CacheError::Parse)?;
|
||||
}
|
||||
Ok(cache)
|
||||
}
|
||||
async fn set_entire_cache(cache_file: &Path, cache: Vec<CachedAccount>) -> Result<(), CacheError> {
|
||||
log::trace!("saving cache: {:?}", cache);
|
||||
|
||||
if !cache_file.exists() {
|
||||
let cache_file_parent = cache_file
|
||||
.parent()
|
||||
.expect("Cache file is root directory and also doesn't exist.");
|
||||
log::debug!(
|
||||
"Making cache file parent directory at {}",
|
||||
cache_file_parent.to_string_lossy()
|
||||
);
|
||||
std::fs::create_dir_all(cache_file_parent).map_err(CacheError::MkDir)?;
|
||||
}
|
||||
let mut cache_file = File::create(cache_file).await.map_err(CacheError::Write)?;
|
||||
let cache = serde_json::to_string_pretty(&cache).map_err(CacheError::Parse)?;
|
||||
cache_file
|
||||
.write_all(cache.as_bytes())
|
||||
.await
|
||||
.map_err(CacheError::Write)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Gets cached data for the given email.
|
||||
///
|
||||
/// Technically it doesn't actually have to be an email since it's only the
|
||||
/// cache key. I considered using usernames or UUIDs as the cache key, but
|
||||
/// usernames change and no one has their UUID memorized.
|
||||
pub async fn get_account_in_cache(cache_file: &Path, email: &str) -> Option<CachedAccount> {
|
||||
let cache = get_entire_cache(cache_file).await.unwrap_or_default();
|
||||
cache.into_iter().find(|account| account.email == email)
|
||||
}
|
||||
|
||||
pub async fn set_account_in_cache(
|
||||
cache_file: &Path,
|
||||
email: &str,
|
||||
account: CachedAccount,
|
||||
) -> Result<(), CacheError> {
|
||||
let mut cache = get_entire_cache(cache_file).await.unwrap_or_default();
|
||||
cache.retain(|account| account.email != email);
|
||||
cache.push(account);
|
||||
set_entire_cache(cache_file, cache).await
|
||||
}
|
|
@ -1,3 +1,6 @@
|
|||
//! Handle Minecraft authentication.
|
||||
|
||||
mod auth;
|
||||
mod cache;
|
||||
pub mod game_profile;
|
||||
pub mod sessionserver;
|
||||
|
||||
pub use auth::*;
|
||||
|
|
78
azalea-auth/src/sessionserver.rs
Normal file
78
azalea-auth/src/sessionserver.rs
Normal file
|
@ -0,0 +1,78 @@
|
|||
//! Tell Mojang you're joining a multiplayer server.
|
||||
//!
|
||||
use serde::Deserialize;
|
||||
use serde_json::json;
|
||||
use thiserror::Error;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum SessionServerError {
|
||||
#[error("Error sending HTTP request to sessionserver: {0}")]
|
||||
HttpError(#[from] reqwest::Error),
|
||||
#[error("Multiplayer is not enabled for this account")]
|
||||
MultiplayerDisabled,
|
||||
#[error("This account has been banned from multiplayer")]
|
||||
Banned,
|
||||
#[error("Unknown sessionserver error: {0}")]
|
||||
Unknown(String),
|
||||
#[error("Unexpected response from sessionserver (status code {status_code}): {body}")]
|
||||
UnexpectedResponse { status_code: u16, body: String },
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct ForbiddenError {
|
||||
pub error: String,
|
||||
pub path: String,
|
||||
}
|
||||
|
||||
/// Tell Mojang's servers that you are going to join a multiplayer server,
|
||||
/// which is required to join online-mode servers. The server ID is an empty
|
||||
/// string.
|
||||
pub async fn join(
|
||||
access_token: &str,
|
||||
public_key: &[u8],
|
||||
private_key: &[u8],
|
||||
uuid: &Uuid,
|
||||
server_id: &str,
|
||||
) -> Result<(), SessionServerError> {
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
let server_hash = azalea_crypto::hex_digest(&azalea_crypto::digest_data(
|
||||
server_id.as_bytes(),
|
||||
public_key,
|
||||
private_key,
|
||||
));
|
||||
|
||||
let mut encode_buffer = Uuid::encode_buffer();
|
||||
let undashed_uuid = uuid.simple().encode_lower(&mut encode_buffer);
|
||||
|
||||
let data = json!({
|
||||
"accessToken": access_token,
|
||||
"selectedProfile": undashed_uuid,
|
||||
"serverId": server_hash
|
||||
});
|
||||
let res = client
|
||||
.post("https://sessionserver.mojang.com/session/minecraft/join")
|
||||
.json(&data)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
match res.status() {
|
||||
reqwest::StatusCode::NO_CONTENT => Ok(()),
|
||||
reqwest::StatusCode::FORBIDDEN => {
|
||||
let forbidden = res.json::<ForbiddenError>().await?;
|
||||
match forbidden.error.as_str() {
|
||||
"InsufficientPrivilegesException" => Err(SessionServerError::MultiplayerDisabled),
|
||||
"UserBannedException" => Err(SessionServerError::Banned),
|
||||
_ => Err(SessionServerError::Unknown(forbidden.error)),
|
||||
}
|
||||
}
|
||||
status_code => {
|
||||
let body = res.text().await?;
|
||||
Err(SessionServerError::UnexpectedResponse {
|
||||
status_code: status_code.as_u16(),
|
||||
body,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,12 +3,12 @@ description = "Representation of Minecraft block states."
|
|||
edition = "2021"
|
||||
license = "MIT"
|
||||
name = "azalea-block"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
|
||||
[lib]
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
azalea-block-macros = {path = "./azalea-block-macros", version = "^0.1.0"}
|
||||
azalea-buf = {path = "../azalea-buf", version = "^0.1.0"}
|
||||
azalea-block-macros = {path = "./azalea-block-macros", version = "^0.2.0" }
|
||||
azalea-buf = {path = "../azalea-buf", version = "^0.2.0" }
|
||||
|
|
|
@ -3,7 +3,7 @@ description = "Proc macros used by azalea-block."
|
|||
edition = "2021"
|
||||
license = "MIT"
|
||||
name = "azalea-block-macros"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
|
2
azalea-brigadier/Cargo.toml
Executable file → Normal file
2
azalea-brigadier/Cargo.toml
Executable file → Normal file
|
@ -3,7 +3,7 @@ description = "A port of Mojang's Brigadier command parsing and dispatching libr
|
|||
edition = "2021"
|
||||
license = "MIT"
|
||||
name = "azalea-brigadier"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
|
|
|
@ -231,7 +231,6 @@ impl<S> CommandDispatcher<S> {
|
|||
for context in contexts.iter() {
|
||||
let child = &context.child;
|
||||
if let Some(child) = child {
|
||||
println!("aaaaaaa {:?}", child);
|
||||
forked |= child.forks;
|
||||
if child.has_nodes() {
|
||||
found_command = true;
|
||||
|
|
|
@ -3,12 +3,12 @@ description = "Serialize and deserialize buffers from Minecraft."
|
|||
edition = "2021"
|
||||
license = "MIT"
|
||||
name = "azalea-buf"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
azalea-buf-macros = {path = "./azalea-buf-macros", version = "^0.1.0"}
|
||||
azalea-buf-macros = {path = "./azalea-buf-macros", version = "^0.2.0" }
|
||||
byteorder = "^1.4.3"
|
||||
serde_json = {version = "^1.0", optional = true}
|
||||
thiserror = "^1.0.34"
|
||||
|
|
|
@ -3,7 +3,7 @@ description = "#[derive(McBuf)]"
|
|||
edition = "2021"
|
||||
license = "MIT"
|
||||
name = "azalea-buf-macros"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use std::ops::Deref;
|
||||
|
||||
/// A Vec<u8> that isn't prefixed by a VarInt with the size.
|
||||
/// A `Vec<u8>` that isn't prefixed by a VarInt with the size.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct UnsizedByteArray(pub Vec<u8>);
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@ const MAX_STRING_LENGTH: u16 = 32767;
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::collections::HashMap;
|
||||
use std::{collections::HashMap, io::Cursor};
|
||||
|
||||
#[test]
|
||||
fn test_write_varint() {
|
||||
|
@ -74,44 +74,72 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_read_varint() {
|
||||
let buf = &mut &vec![0][..];
|
||||
assert_eq!(i32::var_read_from(buf).unwrap(), 0);
|
||||
// let buf = &mut &vec![0][..];
|
||||
// assert_eq!(i32::var_read_from(buf).unwrap(), 0);
|
||||
let buf = vec![0];
|
||||
assert_eq!(i32::var_read_from(&mut Cursor::new(&buf)).unwrap(), 0);
|
||||
|
||||
let buf = &mut &vec![1][..];
|
||||
assert_eq!(i32::var_read_from(buf).unwrap(), 1);
|
||||
// let buf = &mut &vec![1][..];
|
||||
// assert_eq!(i32::var_read_from(buf).unwrap(), 1);
|
||||
let buf = vec![1];
|
||||
assert_eq!(i32::var_read_from(&mut Cursor::new(&buf)).unwrap(), 1);
|
||||
|
||||
let buf = &mut &vec![2][..];
|
||||
assert_eq!(i32::var_read_from(buf).unwrap(), 2);
|
||||
// let buf = &mut &vec![2][..];
|
||||
// assert_eq!(i32::var_read_from(buf).unwrap(), 2);
|
||||
let buf = vec![2];
|
||||
assert_eq!(i32::var_read_from(&mut Cursor::new(&buf)).unwrap(), 2);
|
||||
|
||||
let buf = &mut &vec![127][..];
|
||||
assert_eq!(i32::var_read_from(buf).unwrap(), 127);
|
||||
// let buf = &mut &vec![127][..];
|
||||
// assert_eq!(i32::var_read_from(buf).unwrap(), 127);
|
||||
let buf = vec![127];
|
||||
assert_eq!(i32::var_read_from(&mut Cursor::new(&buf)).unwrap(), 127);
|
||||
|
||||
let buf = &mut &vec![128, 1][..];
|
||||
assert_eq!(i32::var_read_from(buf).unwrap(), 128);
|
||||
// let buf = &mut &vec![128, 1][..];
|
||||
// assert_eq!(i32::var_read_from(buf).unwrap(), 128);
|
||||
let buf = vec![128, 1];
|
||||
assert_eq!(i32::var_read_from(&mut Cursor::new(&buf)).unwrap(), 128);
|
||||
|
||||
let buf = &mut &vec![255, 1][..];
|
||||
assert_eq!(i32::var_read_from(buf).unwrap(), 255);
|
||||
// let buf = &mut &vec![255, 1][..];
|
||||
// assert_eq!(i32::var_read_from(buf).unwrap(), 255);
|
||||
let buf = vec![255, 1];
|
||||
assert_eq!(i32::var_read_from(&mut Cursor::new(&buf)).unwrap(), 255);
|
||||
|
||||
let buf = &mut &vec![221, 199, 1][..];
|
||||
assert_eq!(i32::var_read_from(buf).unwrap(), 25565);
|
||||
// let buf = &mut &vec![221, 199, 1][..];
|
||||
// assert_eq!(i32::var_read_from(buf).unwrap(), 25565);
|
||||
let buf = vec![221, 199, 1];
|
||||
assert_eq!(i32::var_read_from(&mut Cursor::new(&buf)).unwrap(), 25565);
|
||||
|
||||
let buf = &mut &vec![255, 255, 127][..];
|
||||
assert_eq!(i32::var_read_from(buf).unwrap(), 2097151);
|
||||
// let buf = &mut &vec![255, 255, 127][..];
|
||||
// assert_eq!(i32::var_read_from(buf).unwrap(), 2097151);
|
||||
let buf = vec![255, 255, 127];
|
||||
assert_eq!(i32::var_read_from(&mut Cursor::new(&buf)).unwrap(), 2097151);
|
||||
|
||||
let buf = &mut &vec![255, 255, 255, 255, 7][..];
|
||||
assert_eq!(i32::var_read_from(buf).unwrap(), 2147483647);
|
||||
// let buf = &mut &vec![255, 255, 255, 255, 7][..];
|
||||
// assert_eq!(i32::var_read_from(buf).unwrap(), 2147483647);
|
||||
let buf = vec![255, 255, 255, 255, 7];
|
||||
assert_eq!(
|
||||
i32::var_read_from(&mut Cursor::new(&buf)).unwrap(),
|
||||
2147483647
|
||||
);
|
||||
|
||||
let buf = &mut &vec![255, 255, 255, 255, 15][..];
|
||||
assert_eq!(i32::var_read_from(buf).unwrap(), -1);
|
||||
// let buf = &mut &vec![255, 255, 255, 255, 15][..];
|
||||
// assert_eq!(i32::var_read_from(buf).unwrap(), -1);
|
||||
let buf = vec![255, 255, 255, 255, 15];
|
||||
assert_eq!(i32::var_read_from(&mut Cursor::new(&buf)).unwrap(), -1);
|
||||
|
||||
let buf = &mut &vec![128, 128, 128, 128, 8][..];
|
||||
assert_eq!(i32::var_read_from(buf).unwrap(), -2147483648);
|
||||
// let buf = &mut &vec![128, 128, 128, 128, 8][..];
|
||||
// assert_eq!(i32::var_read_from(buf).unwrap(), -2147483648);
|
||||
let buf = vec![128, 128, 128, 128, 8];
|
||||
assert_eq!(
|
||||
i32::var_read_from(&mut Cursor::new(&buf)).unwrap(),
|
||||
-2147483648
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_read_varint_longer() {
|
||||
let buf = &mut &vec![138, 56, 0, 135, 56, 123][..];
|
||||
assert_eq!(i32::var_read_from(buf).unwrap(), 7178);
|
||||
let buf = vec![138, 56, 0, 135, 56, 123];
|
||||
assert_eq!(i32::var_read_from(&mut Cursor::new(&buf)).unwrap(), 7178);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -123,8 +151,7 @@ mod tests {
|
|||
|
||||
dbg!(&buf);
|
||||
|
||||
let buf = &mut &buf[..];
|
||||
let result = Vec::<String>::read_from(buf).unwrap();
|
||||
let result = Vec::<String>::read_from(&mut Cursor::new(&buf)).unwrap();
|
||||
assert_eq!(result, original_vec);
|
||||
}
|
||||
|
||||
|
@ -133,9 +160,7 @@ mod tests {
|
|||
let mut buf = Vec::new();
|
||||
vec![1, 2, 3].var_write_into(&mut buf).unwrap();
|
||||
|
||||
let buf = &mut &buf[..];
|
||||
|
||||
let result = Vec::<i32>::var_read_from(buf).unwrap();
|
||||
let result = Vec::<i32>::var_read_from(&mut Cursor::new(&buf)).unwrap();
|
||||
assert_eq!(result, vec![1, 2, 3]);
|
||||
}
|
||||
|
||||
|
@ -149,19 +174,16 @@ mod tests {
|
|||
let mut buf = Vec::new();
|
||||
original_map.var_write_into(&mut buf).unwrap();
|
||||
|
||||
let buf = &mut &buf[..];
|
||||
|
||||
let result = HashMap::<String, i32>::var_read_from(buf).unwrap();
|
||||
let result = HashMap::<String, i32>::var_read_from(&mut Cursor::new(&buf)).unwrap();
|
||||
|
||||
assert_eq!(result, original_map);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_long() {
|
||||
let buf: &mut Vec<u8> = &mut Vec::new();
|
||||
123456u64.write_into(buf).unwrap();
|
||||
let mut buf: Vec<u8> = Vec::new();
|
||||
123456u64.write_into(&mut buf).unwrap();
|
||||
|
||||
let buf = &mut &buf[..];
|
||||
assert_eq!(u64::read_from(buf).unwrap(), 123456);
|
||||
assert_eq!(u64::read_from(&mut Cursor::new(&buf)).unwrap(), 123456);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -57,7 +57,7 @@ fn read_bytes<'a>(buf: &'a mut Cursor<&[u8]>, length: usize) -> Result<&'a [u8],
|
|||
fn read_utf_with_len(buf: &mut Cursor<&[u8]>, max_length: u32) -> Result<String, BufReadError> {
|
||||
let length = u32::var_read_from(buf)?;
|
||||
// i don't know why it's multiplied by 4 but it's like that in mojang's code so
|
||||
if length as u32 > max_length * 4 {
|
||||
if length > max_length * 4 {
|
||||
return Err(BufReadError::StringLengthTooLong {
|
||||
length,
|
||||
max_length: max_length * 4,
|
||||
|
|
|
@ -110,6 +110,12 @@ impl McBufWritable for String {
|
|||
}
|
||||
}
|
||||
|
||||
impl McBufWritable for &str {
|
||||
fn write_into(&self, buf: &mut impl Write) -> Result<(), std::io::Error> {
|
||||
write_utf_with_len(buf, self, MAX_STRING_LENGTH.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl McBufWritable for u32 {
|
||||
fn write_into(&self, buf: &mut impl Write) -> Result<(), std::io::Error> {
|
||||
i32::write_into(&(*self as i32), buf)
|
||||
|
|
6
azalea-chat/Cargo.toml
Executable file → Normal file
6
azalea-chat/Cargo.toml
Executable file → Normal file
|
@ -3,13 +3,13 @@ description = "Parse Minecraft chat messages."
|
|||
edition = "2021"
|
||||
license = "MIT"
|
||||
name = "azalea-chat"
|
||||
version = "0.1.1"
|
||||
version = "0.2.0"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
azalea-buf = {path = "../azalea-buf", features = ["serde_json"], version = "^0.1.0"}
|
||||
azalea-language = {path = "../azalea-language", version = "^0.1.0"}
|
||||
azalea-buf = {path = "../azalea-buf", features = ["serde_json"], version = "^0.2.0" }
|
||||
azalea-language = {path = "../azalea-language", version = "^0.2.0" }
|
||||
lazy_static = "^1.4.0"
|
||||
serde = "^1.0.130"
|
||||
serde_json = "^1.0.72"
|
||||
|
|
|
@ -267,16 +267,9 @@ impl From<String> for Component {
|
|||
|
||||
impl Display for Component {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
// this contains the final string will all the ansi escape codes
|
||||
for component in self.clone().into_iter() {
|
||||
let component_text = match &component {
|
||||
Self::Text(c) => c.text.to_string(),
|
||||
Self::Translatable(c) => c.to_string(),
|
||||
};
|
||||
|
||||
f.write_str(&component_text)?;
|
||||
match self {
|
||||
Component::Text(c) => c.fmt(f),
|
||||
Component::Translatable(c) => c.fmt(f),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -274,7 +274,7 @@ impl TryFrom<ChatFormatting> for TextColor {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Style {
|
||||
// these are options instead of just bools because None is different than false in this case
|
||||
pub color: Option<TextColor>,
|
||||
|
@ -288,20 +288,8 @@ pub struct Style {
|
|||
}
|
||||
|
||||
impl Style {
|
||||
pub fn default() -> Self {
|
||||
Self::empty()
|
||||
}
|
||||
|
||||
pub fn empty() -> Self {
|
||||
Self {
|
||||
color: None,
|
||||
bold: None,
|
||||
italic: None,
|
||||
underlined: None,
|
||||
strikethrough: None,
|
||||
obfuscated: None,
|
||||
reset: false,
|
||||
}
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn deserialize(json: &Value) -> Style {
|
||||
|
|
22
azalea-chat/src/text_component.rs
Executable file → Normal file
22
azalea-chat/src/text_component.rs
Executable file → Normal file
|
@ -1,4 +1,4 @@
|
|||
use std::fmt;
|
||||
use std::fmt::Display;
|
||||
|
||||
use crate::{base_component::BaseComponent, component::Component, style::ChatFormatting};
|
||||
|
||||
|
@ -52,6 +52,10 @@ pub fn legacy_color_code_to_text_component(legacy_color_code: &str) -> TextCompo
|
|||
i += 1;
|
||||
}
|
||||
|
||||
if components.is_empty() {
|
||||
return TextComponent::new("".to_string());
|
||||
}
|
||||
|
||||
// create the final component by using the first one as the base, and then adding the rest as siblings
|
||||
let mut final_component = components.remove(0);
|
||||
for component in components {
|
||||
|
@ -79,9 +83,19 @@ impl TextComponent {
|
|||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for TextComponent {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}", self.text.clone())
|
||||
impl Display for TextComponent {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
// this contains the final string will all the ansi escape codes
|
||||
for component in Component::Text(self.clone()).into_iter() {
|
||||
let component_text = match &component {
|
||||
Component::Text(c) => c.text.to_string(),
|
||||
Component::Translatable(c) => c.read()?.to_string(),
|
||||
};
|
||||
|
||||
f.write_str(&component_text)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
97
azalea-chat/src/translatable_component.rs
Executable file → Normal file
97
azalea-chat/src/translatable_component.rs
Executable file → Normal file
|
@ -1,6 +1,9 @@
|
|||
use std::fmt::{self, Formatter};
|
||||
use std::fmt::{self, Display, Formatter};
|
||||
|
||||
use crate::{base_component::BaseComponent, component::Component};
|
||||
use crate::{
|
||||
base_component::BaseComponent, component::Component, style::Style,
|
||||
text_component::TextComponent,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum StringOrComponent {
|
||||
|
@ -24,38 +27,43 @@ impl TranslatableComponent {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn read(&self) -> Result<String, fmt::Error> {
|
||||
/// Convert the key and args to a Component.
|
||||
pub fn read(&self) -> Result<TextComponent, fmt::Error> {
|
||||
let template = azalea_language::get(&self.key).unwrap_or(&self.key);
|
||||
// decode the % things
|
||||
|
||||
let mut result = String::new();
|
||||
let mut i = 0;
|
||||
let mut matched = 0;
|
||||
|
||||
// this code is ugly but it works
|
||||
// every time we get a char we add it to built_text, and we push it to
|
||||
// `arguments` and clear it when we add a new argument component
|
||||
let mut built_text = String::new();
|
||||
let mut components = Vec::new();
|
||||
|
||||
while i < template.len() {
|
||||
if template.chars().nth(i).unwrap() == '%' {
|
||||
let char_after = match template.chars().nth(i + 1) {
|
||||
Some(c) => c,
|
||||
None => {
|
||||
result.push(template.chars().nth(i).unwrap());
|
||||
built_text.push(template.chars().nth(i).unwrap());
|
||||
break;
|
||||
}
|
||||
};
|
||||
i += 1;
|
||||
match char_after {
|
||||
'%' => {
|
||||
result.push('%');
|
||||
built_text.push('%');
|
||||
}
|
||||
's' => {
|
||||
result.push_str(
|
||||
&self
|
||||
.args
|
||||
.get(matched)
|
||||
.unwrap_or(&StringOrComponent::String("".to_string()))
|
||||
.to_string(),
|
||||
);
|
||||
let arg_component = self
|
||||
.args
|
||||
.get(matched)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| StringOrComponent::String("".to_string()));
|
||||
|
||||
components.push(TextComponent::new(built_text.clone()));
|
||||
built_text.clear();
|
||||
components.push(TextComponent::from(arg_component));
|
||||
matched += 1;
|
||||
}
|
||||
_ => {
|
||||
|
@ -65,7 +73,7 @@ impl TranslatableComponent {
|
|||
if let Some('$') = template.chars().nth(i + 1) {
|
||||
if let Some('s') = template.chars().nth(i + 2) {
|
||||
i += 2;
|
||||
result.push_str(
|
||||
built_text.push_str(
|
||||
&self
|
||||
.args
|
||||
.get((d - 1) as usize)
|
||||
|
@ -80,32 +88,63 @@ impl TranslatableComponent {
|
|||
}
|
||||
} else {
|
||||
i -= 1;
|
||||
result.push('%');
|
||||
built_text.push('%');
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
result.push(template.chars().nth(i).unwrap());
|
||||
built_text.push(template.chars().nth(i).unwrap());
|
||||
}
|
||||
|
||||
i += 1
|
||||
}
|
||||
|
||||
Ok(result.to_string())
|
||||
if components.is_empty() {
|
||||
return Ok(TextComponent::new(built_text));
|
||||
}
|
||||
|
||||
components.push(TextComponent::new(built_text));
|
||||
|
||||
Ok(TextComponent {
|
||||
base: BaseComponent {
|
||||
siblings: components.into_iter().map(Component::Text).collect(),
|
||||
style: Style::default(),
|
||||
},
|
||||
text: "".to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for TranslatableComponent {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> {
|
||||
write!(f, "{}", self.read()?)
|
||||
impl Display for TranslatableComponent {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
// this contains the final string will all the ansi escape codes
|
||||
for component in Component::Translatable(self.clone()).into_iter() {
|
||||
let component_text = match &component {
|
||||
Component::Text(c) => c.text.to_string(),
|
||||
Component::Translatable(c) => c.read()?.to_string(),
|
||||
};
|
||||
|
||||
f.write_str(&component_text)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for StringOrComponent {
|
||||
impl Display for StringOrComponent {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> {
|
||||
match self {
|
||||
StringOrComponent::String(s) => write!(f, "{}", s),
|
||||
StringOrComponent::Component(c) => write!(f, "{}", c.to_ansi(None)),
|
||||
StringOrComponent::Component(c) => write!(f, "{}", c),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<StringOrComponent> for TextComponent {
|
||||
fn from(soc: StringOrComponent) -> Self {
|
||||
match soc {
|
||||
StringOrComponent::String(s) => TextComponent::new(s),
|
||||
StringOrComponent::Component(c) => TextComponent::new(c.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -118,7 +157,7 @@ mod tests {
|
|||
#[test]
|
||||
fn test_none() {
|
||||
let c = TranslatableComponent::new("translation.test.none".to_string(), vec![]);
|
||||
assert_eq!(c.read(), Ok("Hello, world!".to_string()));
|
||||
assert_eq!(c.read().unwrap().to_string(), "Hello, world!".to_string());
|
||||
}
|
||||
#[test]
|
||||
fn test_complex() {
|
||||
|
@ -133,8 +172,8 @@ mod tests {
|
|||
);
|
||||
// so true mojang
|
||||
assert_eq!(
|
||||
c.read(),
|
||||
Ok("Prefix, ab again b and a lastly c and also a again!".to_string())
|
||||
c.read().unwrap().to_string(),
|
||||
"Prefix, ab again b and a lastly c and also a again!".to_string()
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
|
@ -148,7 +187,7 @@ mod tests {
|
|||
StringOrComponent::String("d".to_string()),
|
||||
],
|
||||
);
|
||||
assert_eq!(c.read(), Ok("%s %a %%s %%b".to_string()));
|
||||
assert_eq!(c.read().unwrap().to_string(), "%s %a %%s %%b".to_string());
|
||||
}
|
||||
#[test]
|
||||
fn test_invalid() {
|
||||
|
@ -161,7 +200,7 @@ mod tests {
|
|||
StringOrComponent::String("d".to_string()),
|
||||
],
|
||||
);
|
||||
assert_eq!(c.read(), Ok("hi %".to_string()));
|
||||
assert_eq!(c.read().unwrap().to_string(), "hi %".to_string());
|
||||
}
|
||||
#[test]
|
||||
fn test_invalid2() {
|
||||
|
@ -174,6 +213,6 @@ mod tests {
|
|||
StringOrComponent::String("d".to_string()),
|
||||
],
|
||||
);
|
||||
assert_eq!(c.read(), Ok("hi % s".to_string()));
|
||||
assert_eq!(c.read().unwrap().to_string(), "hi % s".to_string());
|
||||
}
|
||||
}
|
||||
|
|
22
azalea-client/Cargo.toml
Executable file → Normal file
22
azalea-client/Cargo.toml
Executable file → Normal file
|
@ -1,22 +1,24 @@
|
|||
[package]
|
||||
description = "A headless Minecraft client."
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
name = "azalea-client"
|
||||
version = "0.1.0"
|
||||
version = "0.2.2"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.59"
|
||||
azalea-auth = {path = "../azalea-auth"}
|
||||
azalea-block = {path = "../azalea-block"}
|
||||
azalea-chat = {path = "../azalea-chat"}
|
||||
azalea-core = {path = "../azalea-core"}
|
||||
azalea-crypto = {path = "../azalea-crypto"}
|
||||
azalea-physics = {path = "../azalea-physics"}
|
||||
azalea-protocol = {path = "../azalea-protocol"}
|
||||
azalea-world = {path = "../azalea-world"}
|
||||
azalea-auth = { path = "../azalea-auth", version = "0.2.1" }
|
||||
azalea-block = { path = "../azalea-block", version = "0.2.0" }
|
||||
azalea-chat = { path = "../azalea-chat", version = "0.2.0" }
|
||||
azalea-core = { path = "../azalea-core", version = "0.2.0" }
|
||||
azalea-crypto = { path = "../azalea-crypto", version = "0.2.0" }
|
||||
azalea-physics = { path = "../azalea-physics", version = "0.2.0" }
|
||||
azalea-protocol = { path = "../azalea-protocol", version = "0.2.0" }
|
||||
azalea-world = { path = "../azalea-world", version = "0.2.0" }
|
||||
log = "0.4.17"
|
||||
parking_lot = "0.12.1"
|
||||
thiserror = "^1.0.34"
|
||||
tokio = {version = "^1.19.2", features = ["sync"]}
|
||||
tokio = { version = "^1.19.2", features = ["sync"] }
|
||||
uuid = "^1.1.2"
|
||||
|
|
|
@ -1,25 +1,52 @@
|
|||
//! Connect to Minecraft servers.
|
||||
|
||||
use crate::{client::JoinError, Client, Event};
|
||||
use azalea_protocol::ServerAddress;
|
||||
use tokio::sync::mpsc::UnboundedReceiver;
|
||||
use crate::get_mc_dir;
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Something that can join Minecraft servers.
|
||||
///
|
||||
/// To join a server using this account, use [`crate::Client::join`].
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Account {
|
||||
/// The Minecraft username of the account.
|
||||
pub username: String,
|
||||
/// The access token for authentication. You can obtain one of these
|
||||
/// manually from azalea-auth.
|
||||
pub access_token: Option<String>,
|
||||
/// Only required for online-mode accounts.
|
||||
pub uuid: Option<uuid::Uuid>,
|
||||
}
|
||||
|
||||
impl Account {
|
||||
/// An offline account does not authenticate with Microsoft's servers, and
|
||||
/// as such can only join offline mode servers. This is useful for testing
|
||||
/// in LAN worlds.
|
||||
pub fn offline(username: &str) -> Self {
|
||||
Self {
|
||||
username: username.to_string(),
|
||||
access_token: None,
|
||||
uuid: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Joins the Minecraft server on the given address using this account.
|
||||
pub async fn join(
|
||||
&self,
|
||||
address: &ServerAddress,
|
||||
) -> Result<(Client, UnboundedReceiver<Event>), JoinError> {
|
||||
Client::join(self, address).await
|
||||
/// This will create an online-mode account by authenticating with
|
||||
/// Microsoft's servers. Note that the email given is actually only used as
|
||||
/// a key for the cache, but it's recommended to use the real email to
|
||||
/// avoid confusion.
|
||||
pub async fn microsoft(email: &str) -> Result<Self, azalea_auth::AuthError> {
|
||||
let minecraft_dir = get_mc_dir::minecraft_dir().unwrap();
|
||||
let auth_result = azalea_auth::auth(
|
||||
email,
|
||||
azalea_auth::AuthOpts {
|
||||
cache_file: Some(minecraft_dir.join("azalea-auth.json")),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
Ok(Self {
|
||||
username: auth_result.profile.name,
|
||||
access_token: Some(auth_result.access_token),
|
||||
uuid: Some(Uuid::parse_str(&auth_result.profile.id).expect("Invalid UUID")),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
103
azalea-client/src/chat.rs
Normal file
103
azalea-client/src/chat.rs
Normal file
|
@ -0,0 +1,103 @@
|
|||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use azalea_crypto::MessageSignature;
|
||||
use azalea_protocol::packets::game::{
|
||||
clientbound_player_chat_packet::LastSeenMessagesUpdate,
|
||||
serverbound_chat_command_packet::ServerboundChatCommandPacket,
|
||||
serverbound_chat_packet::ServerboundChatPacket,
|
||||
};
|
||||
|
||||
use crate::Client;
|
||||
|
||||
impl Client {
|
||||
/// Sends chat message to the server. This only sends the chat packet and
|
||||
/// not the command packet. The [`Client::chat`] function handles checking whether
|
||||
/// the message is a command and using the proper packet for you, so you
|
||||
/// should use that instead.
|
||||
pub async fn send_chat_packet(&self, message: &str) -> Result<(), std::io::Error> {
|
||||
// TODO: chat signing
|
||||
let signature = sign_message();
|
||||
let packet = ServerboundChatPacket {
|
||||
message: message.to_string(),
|
||||
timestamp: SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.expect("Time shouldn't be before epoch")
|
||||
.as_millis()
|
||||
.try_into()
|
||||
.expect("Instant should fit into a u64"),
|
||||
salt: azalea_crypto::make_salt(),
|
||||
signature,
|
||||
signed_preview: false,
|
||||
last_seen_messages: LastSeenMessagesUpdate::default(),
|
||||
}
|
||||
.get();
|
||||
self.write_packet(packet).await
|
||||
}
|
||||
|
||||
/// Send a command packet to the server. The `command` argument should not
|
||||
/// include the slash at the front.
|
||||
pub async fn send_command_packet(&self, command: &str) -> Result<(), std::io::Error> {
|
||||
// TODO: chat signing
|
||||
let packet = ServerboundChatCommandPacket {
|
||||
command: command.to_string(),
|
||||
timestamp: SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.expect("Time shouldn't be before epoch")
|
||||
.as_millis()
|
||||
.try_into()
|
||||
.expect("Instant should fit into a u64"),
|
||||
salt: azalea_crypto::make_salt(),
|
||||
argument_signatures: vec![],
|
||||
signed_preview: false,
|
||||
last_seen_messages: LastSeenMessagesUpdate::default(),
|
||||
}
|
||||
.get();
|
||||
self.write_packet(packet).await
|
||||
}
|
||||
|
||||
/// Send a message in chat.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use azalea::prelude::*;
|
||||
/// # use parking_lot::Mutex;
|
||||
/// # use std::sync::Arc;
|
||||
/// # #[tokio::main]
|
||||
/// # async fn main() {
|
||||
/// # let account = Account::offline("bot");
|
||||
/// # azalea::start(azalea::Options {
|
||||
/// # account,
|
||||
/// # address: "localhost",
|
||||
/// # state: State::default(),
|
||||
/// # plugins: vec![],
|
||||
/// # handle,
|
||||
/// # })
|
||||
/// # .await
|
||||
/// # .unwrap();
|
||||
/// # }
|
||||
/// # #[derive(Default, Clone)]
|
||||
/// # pub struct State {}
|
||||
/// # async fn handle(bot: Client, event: Event, state: State) -> anyhow::Result<()> {
|
||||
/// bot.chat("Hello, world!").await.unwrap();
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub async fn chat(&self, message: &str) -> Result<(), std::io::Error> {
|
||||
if let Some(command) = message.strip_prefix('/') {
|
||||
self.send_command_packet(command).await
|
||||
} else {
|
||||
self.send_chat_packet(message).await
|
||||
}
|
||||
}
|
||||
|
||||
// will be used for when the server tells the client about a chat preview
|
||||
// with custom formatting
|
||||
// pub fn acknowledge_preview(&self, message: &str) {}
|
||||
}
|
||||
|
||||
// TODO
|
||||
// MessageSigner, ChatMessageContent, LastSeenMessages
|
||||
fn sign_message() -> MessageSignature {
|
||||
MessageSignature::default()
|
||||
}
|
|
@ -9,6 +9,7 @@ use azalea_protocol::{
|
|||
clientbound_player_chat_packet::ClientboundPlayerChatPacket,
|
||||
clientbound_system_chat_packet::ClientboundSystemChatPacket,
|
||||
serverbound_accept_teleportation_packet::ServerboundAcceptTeleportationPacket,
|
||||
serverbound_client_information_packet::ServerboundClientInformationPacket,
|
||||
serverbound_custom_payload_packet::ServerboundCustomPayloadPacket,
|
||||
serverbound_keep_alive_packet::ServerboundKeepAlivePacket,
|
||||
serverbound_move_player_pos_rot_packet::ServerboundMovePlayerPosRotPacket,
|
||||
|
@ -30,7 +31,7 @@ use azalea_world::{
|
|||
Dimension,
|
||||
};
|
||||
use log::{debug, error, warn};
|
||||
use parking_lot::{Mutex, MutexGuard};
|
||||
use parking_lot::{Mutex, MutexGuard, RwLock};
|
||||
use std::{
|
||||
fmt::Debug,
|
||||
io::{self, Cursor},
|
||||
|
@ -38,16 +39,22 @@ use std::{
|
|||
};
|
||||
use thiserror::Error;
|
||||
use tokio::{
|
||||
io::AsyncWriteExt,
|
||||
sync::mpsc::{self, UnboundedReceiver, UnboundedSender},
|
||||
task::JoinHandle,
|
||||
time::{self},
|
||||
};
|
||||
|
||||
pub type ClientInformation = ServerboundClientInformationPacket;
|
||||
|
||||
/// Events are sent before they're processed, so for example game ticks happen
|
||||
/// at the beginning of a tick before anything has happened.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Event {
|
||||
/// Happens right after the bot switches into the Game state, but before
|
||||
/// it's actually spawned. This can be useful for setting the client
|
||||
/// information with `Client::set_client_information`, so the packet
|
||||
/// doesn't have to be sent twice.
|
||||
Initialize,
|
||||
Login,
|
||||
Chat(ChatPacket),
|
||||
/// Happens 20 times per second, but only when the world is loaded.
|
||||
|
@ -65,12 +72,12 @@ impl ChatPacket {
|
|||
pub fn message(&self) -> Component {
|
||||
match self {
|
||||
ChatPacket::System(p) => p.content.clone(),
|
||||
ChatPacket::Player(p) => p.message.message(false),
|
||||
ChatPacket::Player(p) => p.message(false),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A player that you can control that is currently in a Minecraft server.
|
||||
/// A player that you control that is currently in a Minecraft server.
|
||||
#[derive(Clone)]
|
||||
pub struct Client {
|
||||
game_profile: GameProfile,
|
||||
|
@ -79,6 +86,7 @@ pub struct Client {
|
|||
pub player: Arc<Mutex<Player>>,
|
||||
pub dimension: Arc<Mutex<Dimension>>,
|
||||
pub physics_state: Arc<Mutex<PhysicsState>>,
|
||||
pub client_information: Arc<RwLock<ClientInformation>>,
|
||||
tasks: Arc<Mutex<Vec<JoinHandle<()>>>>,
|
||||
}
|
||||
|
||||
|
@ -105,6 +113,10 @@ pub enum JoinError {
|
|||
ReadPacket(#[from] azalea_protocol::read::ReadPacketError),
|
||||
#[error("{0}")]
|
||||
Io(#[from] io::Error),
|
||||
#[error("{0}")]
|
||||
SessionServer(#[from] azalea_auth::sessionserver::SessionServerError),
|
||||
#[error("The given address could not be parsed into a ServerAddress")]
|
||||
InvalidAddress,
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
|
@ -118,12 +130,30 @@ pub enum HandleError {
|
|||
}
|
||||
|
||||
impl Client {
|
||||
/// Connect to a Minecraft server with an account.
|
||||
/// Connect to a Minecraft server.
|
||||
///
|
||||
/// To change the render distance and other settings, use [`Client::set_client_information`].
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use azalea_client::Client;
|
||||
///
|
||||
/// #[tokio::main]
|
||||
/// async fn main() -> Box<dyn std::error::Error> {
|
||||
/// let account = Account::offline("bot");
|
||||
/// let client = Client::join(&account, "localhost").await?;
|
||||
/// client.chat("Hello, world!").await?;
|
||||
/// client.shutdown().await?;
|
||||
/// }
|
||||
/// ```
|
||||
pub async fn join(
|
||||
account: &Account,
|
||||
address: &ServerAddress,
|
||||
address: impl TryInto<ServerAddress>,
|
||||
) -> Result<(Self, UnboundedReceiver<Event>), JoinError> {
|
||||
let resolved_address = resolver::resolve_address(address).await?;
|
||||
let address: ServerAddress = address.try_into().map_err(|_| JoinError::InvalidAddress)?;
|
||||
|
||||
let resolved_address = resolver::resolve_address(&address).await?;
|
||||
|
||||
let mut conn = Connection::new(&resolved_address).await?;
|
||||
|
||||
|
@ -159,7 +189,17 @@ impl Client {
|
|||
debug!("Got encryption request");
|
||||
let e = azalea_crypto::encrypt(&p.public_key, &p.nonce).unwrap();
|
||||
|
||||
// TODO: authenticate with the server here (authenticateServer)
|
||||
if let Some(access_token) = &account.access_token {
|
||||
conn.authenticate(
|
||||
access_token,
|
||||
&account
|
||||
.uuid
|
||||
.expect("Uuid must be present if access token is present."),
|
||||
e.secret_key,
|
||||
p,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
conn.write(
|
||||
ServerboundKeyPacket {
|
||||
|
@ -171,6 +211,7 @@ impl Client {
|
|||
.get(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
conn.set_encryption_key(e.secret_key);
|
||||
}
|
||||
ClientboundLoginPacket::LoginCompression(p) => {
|
||||
|
@ -210,8 +251,11 @@ impl Client {
|
|||
dimension: Arc::new(Mutex::new(Dimension::default())),
|
||||
physics_state: Arc::new(Mutex::new(PhysicsState::default())),
|
||||
tasks: Arc::new(Mutex::new(Vec::new())),
|
||||
client_information: Arc::new(RwLock::new(ClientInformation::default())),
|
||||
};
|
||||
|
||||
tx.send(Event::Initialize).unwrap();
|
||||
|
||||
// just start up the game loop and we're ready!
|
||||
|
||||
// if you get an error right here that means you're doing something with locks wrong
|
||||
|
@ -237,7 +281,7 @@ impl Client {
|
|||
|
||||
/// Disconnect from the server, ending all tasks.
|
||||
pub async fn shutdown(self) -> Result<(), std::io::Error> {
|
||||
self.write_conn.lock().await.write_stream.shutdown().await?;
|
||||
self.write_conn.lock().await.shutdown().await?;
|
||||
let tasks = self.tasks.lock();
|
||||
for task in tasks.iter() {
|
||||
task.abort();
|
||||
|
@ -359,6 +403,12 @@ impl Client {
|
|||
player_lock.set_entity_id(p.player_id);
|
||||
}
|
||||
|
||||
// send the client information that we have set
|
||||
let client_information_packet: ClientInformation =
|
||||
client.client_information.read().clone();
|
||||
client.write_packet(client_information_packet.get()).await?;
|
||||
|
||||
// brand
|
||||
client
|
||||
.write_packet(
|
||||
ServerboundCustomPayloadPacket {
|
||||
|
@ -633,7 +683,7 @@ impl Client {
|
|||
debug!("Got section blocks update packet {:?}", p);
|
||||
let mut dimension = client.dimension.lock();
|
||||
for state in &p.states {
|
||||
dimension.set_block_state(&(p.section_pos + state.pos), state.state);
|
||||
dimension.set_block_state(&(p.section_pos + state.pos.clone()), state.state);
|
||||
}
|
||||
}
|
||||
ClientboundGamePacket::GameEvent(p) => {
|
||||
|
@ -788,6 +838,35 @@ impl Client {
|
|||
let entity_ptr = unsafe { entity_data.as_const_ptr() };
|
||||
EntityRef::new(dimension, entity_id, entity_ptr)
|
||||
}
|
||||
|
||||
/// Returns whether we have a received the login packet yet.
|
||||
pub fn logged_in(&self) -> bool {
|
||||
let dimension = self.dimension.lock();
|
||||
let player = self.player.lock();
|
||||
player.entity(&dimension).is_some()
|
||||
}
|
||||
|
||||
/// Tell the server we changed our game options (i.e. render distance, main hand).
|
||||
/// If this is not set before the login packet, the default will be sent.
|
||||
pub async fn set_client_information(
|
||||
&self,
|
||||
client_information: ServerboundClientInformationPacket,
|
||||
) -> Result<(), std::io::Error> {
|
||||
{
|
||||
let mut client_information_lock = self.client_information.write();
|
||||
*client_information_lock = client_information;
|
||||
}
|
||||
|
||||
if self.logged_in() {
|
||||
let client_information_packet = {
|
||||
let client_information = self.client_information.read();
|
||||
client_information.clone().get()
|
||||
};
|
||||
self.write_packet(client_information_packet).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<std::sync::PoisonError<T>> for HandleError {
|
||||
|
|
34
azalea-client/src/get_mc_dir.rs
Normal file
34
azalea-client/src/get_mc_dir.rs
Normal file
|
@ -0,0 +1,34 @@
|
|||
//! Find out where the user's .minecraft directory is.
|
||||
//!
|
||||
//! Used for the auth cache.
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Return the location of the user's .minecraft directory.
|
||||
///
|
||||
/// Windows: `%appdata%\.minecraft`\
|
||||
/// Mac: `$HOME/Library/Application Support/minecraft`\
|
||||
/// Linux: `$HOME/.minecraft`
|
||||
///
|
||||
/// Anywhere else it'll return None.
|
||||
pub fn minecraft_dir() -> Option<PathBuf> {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let appdata = std::env::var("APPDATA").ok()?;
|
||||
Some(PathBuf::from(appdata).join(".minecraft"))
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let home = std::env::var("HOME").ok()?;
|
||||
Some(PathBuf::from(home).join("Library/Application Support/minecraft"))
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let home = std::env::var("HOME").ok()?;
|
||||
Some(PathBuf::from(home).join(".minecraft"))
|
||||
}
|
||||
#[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
|
||||
{
|
||||
None
|
||||
}
|
||||
}
|
|
@ -1,13 +1,20 @@
|
|||
//! Significantly abstract azalea-protocol so it's actually useable for bots.
|
||||
//! Significantly abstract [`azalea_protocol`] so it's actually useable for
|
||||
//! real clients. If you want to make bots, however, you should use the
|
||||
//! [`azalea`] crate instead.
|
||||
//!
|
||||
//! [`azalea_protocol`]: https://crates.io/crates/azalea-protocol
|
||||
//! [`azalea`]: https://crates.io/crates/azalea
|
||||
|
||||
mod account;
|
||||
mod chat;
|
||||
mod client;
|
||||
mod get_mc_dir;
|
||||
mod movement;
|
||||
pub mod ping;
|
||||
mod player;
|
||||
|
||||
pub use account::Account;
|
||||
pub use client::{Client, Event};
|
||||
pub use client::{Client, ClientInformation, Event};
|
||||
pub use movement::MoveDirection;
|
||||
pub use player::Player;
|
||||
|
||||
|
|
|
@ -144,9 +144,10 @@ impl Client {
|
|||
let mut entity = player
|
||||
.entity_mut(&mut dimension_lock)
|
||||
.ok_or(MovePlayerError::PlayerNotInWorld)?;
|
||||
println!(
|
||||
log::trace!(
|
||||
"move entity bounding box: {} {:?}",
|
||||
entity.id, entity.bounding_box
|
||||
entity.id,
|
||||
entity.bounding_box
|
||||
);
|
||||
|
||||
entity.move_colliding(&MoverType::Own, movement)?;
|
||||
|
@ -154,6 +155,8 @@ impl Client {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Makes the bot do one physics tick. Note that this is already handled
|
||||
/// automatically by the client.
|
||||
pub fn ai_step(&mut self) {
|
||||
self.tick_controls(None);
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
///! Ping Minecraft servers.
|
||||
//! Ping Minecraft servers.
|
||||
|
||||
use azalea_protocol::{
|
||||
connect::{Connection, ConnectionError},
|
||||
packets::{
|
||||
|
@ -25,12 +26,29 @@ pub enum PingError {
|
|||
ReadPacket(#[from] azalea_protocol::read::ReadPacketError),
|
||||
#[error("{0}")]
|
||||
WritePacket(#[from] io::Error),
|
||||
#[error("The given address could not be parsed into a ServerAddress")]
|
||||
InvalidAddress,
|
||||
}
|
||||
|
||||
/// Ping a Minecraft server.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use azalea_client::ping;
|
||||
///
|
||||
/// #[tokio::main]
|
||||
/// async fn main() {
|
||||
/// let response = ping::ping_server("play.hypixel.net").await.unwrap();
|
||||
/// println!("{}", response.description.to_ansi(None));
|
||||
/// }
|
||||
/// ```
|
||||
pub async fn ping_server(
|
||||
address: &ServerAddress,
|
||||
address: impl TryInto<ServerAddress>,
|
||||
) -> Result<ClientboundStatusResponsePacket, PingError> {
|
||||
let resolved_address = resolver::resolve_address(address).await?;
|
||||
let address: ServerAddress = address.try_into().map_err(|_| PingError::InvalidAddress)?;
|
||||
|
||||
let resolved_address = resolver::resolve_address(&address).await?;
|
||||
|
||||
let mut conn = Connection::new(&resolved_address).await?;
|
||||
|
||||
|
|
8
azalea-core/Cargo.toml
Executable file → Normal file
8
azalea-core/Cargo.toml
Executable file → Normal file
|
@ -3,12 +3,12 @@ description = "Miscellaneous things in Azalea."
|
|||
edition = "2021"
|
||||
license = "MIT"
|
||||
name = "azalea-core"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
azalea-buf = {path = "../azalea-buf", version = "^0.1.0"}
|
||||
azalea-chat = {path = "../azalea-chat", version = "^0.1.0"}
|
||||
azalea-nbt = {path = "../azalea-nbt", version = "^0.1.0"}
|
||||
azalea-buf = {path = "../azalea-buf", version = "^0.2.0" }
|
||||
azalea-chat = {path = "../azalea-chat", version = "^0.2.0" }
|
||||
azalea-nbt = {path = "../azalea-nbt", version = "^0.2.0" }
|
||||
uuid = "^1.1.2"
|
||||
|
|
|
@ -136,7 +136,7 @@ impl ChunkBlockPos {
|
|||
}
|
||||
|
||||
/// The coordinates of a block inside a chunk section. Each coordinate must be in the range [0, 15].
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub struct ChunkSectionBlockPos {
|
||||
pub x: u8,
|
||||
pub y: u8,
|
||||
|
|
|
@ -3,13 +3,13 @@ description = "Cryptography features used in Minecraft."
|
|||
edition = "2021"
|
||||
license = "MIT"
|
||||
name = "azalea-crypto"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
aes = "0.8.1"
|
||||
azalea-buf = {path = "../azalea-buf", version = "^0.1.0"}
|
||||
azalea-buf = {path = "../azalea-buf", version = "^0.2.0" }
|
||||
cfb8 = "0.8.1"
|
||||
num-bigint = "^0.4.3"
|
||||
rand = {version = "^0.8.4", features = ["getrandom"]}
|
||||
|
|
|
@ -18,8 +18,8 @@ fn generate_secret_key() -> [u8; 16] {
|
|||
pub fn digest_data(server_id: &[u8], public_key: &[u8], private_key: &[u8]) -> Vec<u8> {
|
||||
let mut digest = Sha1::new();
|
||||
digest.update(server_id);
|
||||
digest.update(public_key);
|
||||
digest.update(private_key);
|
||||
digest.update(public_key);
|
||||
digest.finalize().to_vec()
|
||||
}
|
||||
|
||||
|
|
|
@ -17,3 +17,8 @@ pub struct SignedMessageHeader {
|
|||
pub previous_signature: Option<MessageSignature>,
|
||||
pub sender: Uuid,
|
||||
}
|
||||
|
||||
/// Generates a random u64 to use as a salt
|
||||
pub fn make_salt() -> u64 {
|
||||
rand::random()
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ description = "Translate Minecraft strings from their id."
|
|||
edition = "2021"
|
||||
license = "MIT"
|
||||
name = "azalea-language"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
|
|
4
azalea-nbt/Cargo.toml
Executable file → Normal file
4
azalea-nbt/Cargo.toml
Executable file → Normal file
|
@ -3,13 +3,13 @@ description = "A fast NBT serializer and deserializer."
|
|||
edition = "2021"
|
||||
license = "MIT"
|
||||
name = "azalea-nbt"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
ahash = "^0.8.0"
|
||||
azalea-buf = {path = "../azalea-buf", version = "^0.1.0"}
|
||||
azalea-buf = {path = "../azalea-buf", version = "^0.2.0" }
|
||||
byteorder = "^1.4.3"
|
||||
flate2 = "^1.0.23"
|
||||
num-derive = "^0.3.3"
|
||||
|
|
|
@ -98,7 +98,7 @@ impl Tag {
|
|||
if length * 4 > (stream.get_ref().len() - stream.position() as usize) {
|
||||
return Err(Error::UnexpectedEof);
|
||||
}
|
||||
let mut ints = Vec::with_capacity(length as usize);
|
||||
let mut ints = Vec::with_capacity(length);
|
||||
for _ in 0..length {
|
||||
ints.push(stream.read_i32::<BE>()?);
|
||||
}
|
||||
|
@ -111,7 +111,7 @@ impl Tag {
|
|||
if length * 8 > (stream.get_ref().len() - stream.position() as usize) {
|
||||
return Err(Error::UnexpectedEof);
|
||||
}
|
||||
let mut longs = Vec::with_capacity(length as usize);
|
||||
let mut longs = Vec::with_capacity(length);
|
||||
for _ in 0..length {
|
||||
longs.push(stream.read_i64::<BE>()?);
|
||||
}
|
||||
|
|
|
@ -3,14 +3,14 @@ description = "Physics for Minecraft entities."
|
|||
edition = "2021"
|
||||
license = "MIT"
|
||||
name = "azalea-physics"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
azalea-block = {path = "../azalea-block", version = "^0.1.0"}
|
||||
azalea-core = {path = "../azalea-core", version = "^0.1.0"}
|
||||
azalea-world = {path = "../azalea-world", version = "^0.1.0"}
|
||||
azalea-block = { path = "../azalea-block", version = "^0.2.0" }
|
||||
azalea-core = { path = "../azalea-core", version = "^0.2.0" }
|
||||
azalea-world = { path = "../azalea-world", version = "^0.2.0" }
|
||||
lazy_static = "1.4.0"
|
||||
|
||||
[dev-dependencies]
|
||||
|
|
|
@ -338,8 +338,7 @@ impl From<&DiscreteVoxelShape> for BitSetDiscreteVoxelShape {
|
|||
for y in 0..y_size {
|
||||
for z in 0..z_size {
|
||||
if shape.is_full(x, y, z) {
|
||||
storage
|
||||
.set(Self::get_index_from_size(x, y, z, y_size, z_size) as usize);
|
||||
storage.set(Self::get_index_from_size(x, y, z, y_size, z_size));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -171,9 +171,10 @@ impl Shapes {
|
|||
);
|
||||
let var8 = BitSetDiscreteVoxelShape::join(&a.shape(), &b.shape(), &var5, &var6, &var7, op);
|
||||
// if var5.is_discrete_cube_merger()
|
||||
if let IndexMerger::DiscreteCube { .. } = var5
|
||||
&& let IndexMerger::DiscreteCube { .. } = var6
|
||||
&& let IndexMerger::DiscreteCube { .. } = var7
|
||||
|
||||
if matches!(var5, IndexMerger::DiscreteCube { .. })
|
||||
&& matches!(var6, IndexMerger::DiscreteCube { .. })
|
||||
&& matches!(var7, IndexMerger::DiscreteCube { .. })
|
||||
{
|
||||
VoxelShape::Cube(CubeVoxelShape::new(DiscreteVoxelShape::BitSet(var8)))
|
||||
} else {
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
#![feature(trait_alias)]
|
||||
#![feature(let_chains)]
|
||||
|
||||
pub mod collision;
|
||||
|
||||
|
|
24
azalea-protocol/Cargo.toml
Executable file → Normal file
24
azalea-protocol/Cargo.toml
Executable file → Normal file
|
@ -3,24 +3,24 @@ description = "Send and receive Minecraft packets."
|
|||
edition = "2021"
|
||||
license = "MIT"
|
||||
name = "azalea-protocol"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
async-compression = {version = "^0.3.8", features = ["tokio", "zlib"], optional = true}
|
||||
async-recursion = "1.0.0"
|
||||
azalea-auth = {path = "../azalea-auth", version = "^0.1.0"}
|
||||
azalea-block = {path = "../azalea-block", default-features = false, version = "^0.1.0"}
|
||||
azalea-brigadier = {path = "../azalea-brigadier", version = "^0.1.0"}
|
||||
azalea-buf = {path = "../azalea-buf", version = "^0.1.0"}
|
||||
azalea-chat = {path = "../azalea-chat", version = "^0.1.1"}
|
||||
azalea-core = {path = "../azalea-core", optional = true, version = "^0.1.0"}
|
||||
azalea-crypto = {path = "../azalea-crypto", version = "^0.1.0"}
|
||||
azalea-nbt = {path = "../azalea-nbt", version = "^0.1.0"}
|
||||
azalea-protocol-macros = {path = "./azalea-protocol-macros", version = "^0.1.0"}
|
||||
azalea-registry = {path = "../azalea-registry", version = "^0.1.0"}
|
||||
azalea-world = {path = "../azalea-world", version = "^0.1.0"}
|
||||
azalea-auth = {path = "../azalea-auth", version = "^0.2.1" }
|
||||
azalea-block = {path = "../azalea-block", default-features = false, version = "^0.2.0" }
|
||||
azalea-brigadier = {path = "../azalea-brigadier", version = "^0.2.0" }
|
||||
azalea-buf = {path = "../azalea-buf", version = "^0.2.0" }
|
||||
azalea-chat = {path = "../azalea-chat", version = "^0.2.0" }
|
||||
azalea-core = {path = "../azalea-core", optional = true, version = "^0.2.0" }
|
||||
azalea-crypto = {path = "../azalea-crypto", version = "^0.2.0" }
|
||||
azalea-nbt = {path = "../azalea-nbt", version = "^0.2.0" }
|
||||
azalea-protocol-macros = {path = "./azalea-protocol-macros", version = "^0.2.0" }
|
||||
azalea-registry = {path = "../azalea-registry", version = "^0.2.0" }
|
||||
azalea-world = {path = "../azalea-world", version = "^0.2.0" }
|
||||
byteorder = "^1.4.3"
|
||||
bytes = "^1.1.0"
|
||||
flate2 = "1.0.23"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# Azalea Protocol
|
||||
|
||||
Send and receive Minecraft packets. You should probably use `azalea` or `azalea-client` instead.
|
||||
A low-level crate to send and receive Minecraft packets. You should probably use `azalea` or `azalea-client` instead.
|
||||
|
||||
The goal is to only support the latest Minecraft version in order to ease development.
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ description = "Macros internally used in azalea-protocol."
|
|||
edition = "2021"
|
||||
license = "MIT"
|
||||
name = "azalea-protocol-macros"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
|
|
@ -2,32 +2,36 @@
|
|||
|
||||
use crate::packets::game::{ClientboundGamePacket, ServerboundGamePacket};
|
||||
use crate::packets::handshake::{ClientboundHandshakePacket, ServerboundHandshakePacket};
|
||||
use crate::packets::login::clientbound_hello_packet::ClientboundHelloPacket;
|
||||
use crate::packets::login::{ClientboundLoginPacket, ServerboundLoginPacket};
|
||||
use crate::packets::status::{ClientboundStatusPacket, ServerboundStatusPacket};
|
||||
use crate::packets::ProtocolPacket;
|
||||
use crate::read::{read_packet, ReadPacketError};
|
||||
use crate::write::write_packet;
|
||||
use crate::ServerIpAddress;
|
||||
use azalea_auth::sessionserver::SessionServerError;
|
||||
use azalea_crypto::{Aes128CfbDec, Aes128CfbEnc};
|
||||
use bytes::BytesMut;
|
||||
use std::fmt::Debug;
|
||||
use std::marker::PhantomData;
|
||||
use thiserror::Error;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use tokio::net::tcp::{OwnedReadHalf, OwnedWriteHalf};
|
||||
use tokio::net::TcpStream;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub struct ReadConnection<R: ProtocolPacket> {
|
||||
pub read_stream: OwnedReadHalf,
|
||||
read_stream: OwnedReadHalf,
|
||||
buffer: BytesMut,
|
||||
pub compression_threshold: Option<u32>,
|
||||
pub dec_cipher: Option<Aes128CfbDec>,
|
||||
compression_threshold: Option<u32>,
|
||||
dec_cipher: Option<Aes128CfbDec>,
|
||||
_reading: PhantomData<R>,
|
||||
}
|
||||
|
||||
pub struct WriteConnection<W: ProtocolPacket> {
|
||||
pub write_stream: OwnedWriteHalf,
|
||||
pub compression_threshold: Option<u32>,
|
||||
pub enc_cipher: Option<Aes128CfbEnc>,
|
||||
write_stream: OwnedWriteHalf,
|
||||
compression_threshold: Option<u32>,
|
||||
enc_cipher: Option<Aes128CfbEnc>,
|
||||
_writing: PhantomData<W>,
|
||||
}
|
||||
|
||||
|
@ -64,6 +68,10 @@ where
|
|||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn shutdown(&mut self) -> std::io::Result<()> {
|
||||
self.write_stream.shutdown().await
|
||||
}
|
||||
}
|
||||
|
||||
impl<R, W> Connection<R, W>
|
||||
|
@ -145,13 +153,56 @@ impl Connection<ClientboundLoginPacket, ServerboundLoginPacket> {
|
|||
pub fn set_encryption_key(&mut self, key: [u8; 16]) {
|
||||
// minecraft has a cipher decoder and encoder, i don't think it matters though?
|
||||
let (enc_cipher, dec_cipher) = azalea_crypto::create_cipher(&key);
|
||||
self.writer.enc_cipher = Some(enc_cipher);
|
||||
self.reader.dec_cipher = Some(dec_cipher);
|
||||
self.writer.enc_cipher = Some(enc_cipher);
|
||||
}
|
||||
|
||||
pub fn game(self) -> Connection<ClientboundGamePacket, ServerboundGamePacket> {
|
||||
Connection::from(self)
|
||||
}
|
||||
|
||||
/// Authenticate with Minecraft's servers, which is required to join
|
||||
/// online-mode servers. This must happen when you get a
|
||||
/// `ClientboundLoginPacket::Hello` packet.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// let token = azalea_auth::auth(azalea_auth::AuthOpts {
|
||||
/// ..Default::default()
|
||||
/// })
|
||||
/// .await;
|
||||
/// let player_data = azalea_auth::get_profile(token).await;
|
||||
///
|
||||
/// let mut connection = azalea::Connection::new(&server_address).await?;
|
||||
///
|
||||
/// // transition to the login state, in a real program we would have done a handshake first
|
||||
/// connection.login();
|
||||
///
|
||||
/// match connection.read().await? {
|
||||
/// ClientboundLoginPacket::Hello(p) => {
|
||||
/// // tell Mojang we're joining the server
|
||||
/// connection.authenticate(&token, player_data.uuid, p).await?;
|
||||
/// }
|
||||
/// _ => {}
|
||||
/// }
|
||||
/// ```
|
||||
pub async fn authenticate(
|
||||
&self,
|
||||
access_token: &str,
|
||||
uuid: &Uuid,
|
||||
private_key: [u8; 16],
|
||||
packet: ClientboundHelloPacket,
|
||||
) -> Result<(), SessionServerError> {
|
||||
azalea_auth::sessionserver::join(
|
||||
access_token,
|
||||
&packet.public_key,
|
||||
&private_key,
|
||||
uuid,
|
||||
&packet.server_id,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
// rust doesn't let us implement From because allegedly it conflicts with
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
use azalea_buf::McBuf;
|
||||
use azalea_chat::component::Component;
|
||||
use azalea_chat::{
|
||||
component::Component,
|
||||
translatable_component::{StringOrComponent, TranslatableComponent},
|
||||
};
|
||||
use azalea_core::BitSet;
|
||||
use azalea_crypto::{MessageSignature, SignedMessageHeader};
|
||||
use azalea_protocol_macros::ClientboundGamePacket;
|
||||
|
@ -47,7 +50,10 @@ pub struct SignedMessageBody {
|
|||
}
|
||||
|
||||
impl PlayerChatMessage {
|
||||
pub fn message(&self, only_secure_chat: bool) -> Component {
|
||||
/// Returns the content of the message. If you want to get the Component
|
||||
/// for the whole message including the sender part, use
|
||||
/// [`ClientboundPlayerChatPacket::message`].
|
||||
pub fn content(&self, only_secure_chat: bool) -> Component {
|
||||
if only_secure_chat {
|
||||
return self
|
||||
.signed_body
|
||||
|
@ -58,7 +64,56 @@ impl PlayerChatMessage {
|
|||
}
|
||||
self.unsigned_content
|
||||
.clone()
|
||||
.unwrap_or_else(|| self.message(true))
|
||||
.unwrap_or_else(|| self.content(true))
|
||||
}
|
||||
}
|
||||
|
||||
impl ClientboundPlayerChatPacket {
|
||||
/// Get the full message, including the sender part.
|
||||
pub fn message(&self, only_secure_chat: bool) -> Component {
|
||||
let sender = self.chat_type.name.clone();
|
||||
let content = self.message.content(only_secure_chat);
|
||||
let target = self.chat_type.target_name.clone();
|
||||
|
||||
let translation_key = self.chat_type.chat_type.chat_translation_key();
|
||||
|
||||
let mut args = vec![
|
||||
StringOrComponent::Component(sender),
|
||||
StringOrComponent::Component(content),
|
||||
];
|
||||
if let Some(target) = target {
|
||||
args.push(StringOrComponent::Component(target));
|
||||
}
|
||||
|
||||
let component = TranslatableComponent::new(translation_key.to_string(), args);
|
||||
|
||||
Component::Translatable(component)
|
||||
}
|
||||
}
|
||||
|
||||
impl ChatType {
|
||||
pub fn chat_translation_key(&self) -> &'static str {
|
||||
match self {
|
||||
ChatType::Chat => "chat.type.text",
|
||||
ChatType::SayCommand => "chat.type.announcement",
|
||||
ChatType::MsgCommandIncoming => "commands.message.display.incoming",
|
||||
ChatType::MsgCommandOutgoing => "commands.message.display.outgoing",
|
||||
ChatType::TeamMsgCommandIncoming => "chat.type.team.text",
|
||||
ChatType::TeamMsgCommandOutgoing => "chat.type.team.sent",
|
||||
ChatType::EmoteCommand => "chat.type.emote",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn narrator_translation_key(&self) -> &'static str {
|
||||
match self {
|
||||
ChatType::Chat => "chat.type.text.narrate",
|
||||
ChatType::SayCommand => "chat.type.text.narrate",
|
||||
ChatType::MsgCommandIncoming => "chat.type.text.narrate",
|
||||
ChatType::MsgCommandOutgoing => "chat.type.text.narrate",
|
||||
ChatType::TeamMsgCommandIncoming => "chat.type.text.narrate",
|
||||
ChatType::TeamMsgCommandOutgoing => "chat.type.text.narrate",
|
||||
ChatType::EmoteCommand => "chat.type.emote",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -68,7 +123,7 @@ pub struct LastSeenMessagesEntry {
|
|||
pub last_signature: MessageSignature,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, McBuf)]
|
||||
#[derive(Clone, Debug, McBuf, Default)]
|
||||
pub struct LastSeenMessagesUpdate {
|
||||
pub last_seen: Vec<LastSeenMessagesEntry>,
|
||||
pub last_received: Option<LastSeenMessagesEntry>,
|
||||
|
|
|
@ -22,9 +22,9 @@ pub enum Action {
|
|||
|
||||
#[derive(Clone, Debug, McBuf)]
|
||||
pub struct PlayerProperty {
|
||||
name: String,
|
||||
value: String,
|
||||
signature: Option<String>,
|
||||
pub name: String,
|
||||
pub value: String,
|
||||
pub signature: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, McBuf)]
|
||||
|
@ -42,26 +42,26 @@ pub struct AddPlayer {
|
|||
|
||||
#[derive(Clone, Debug, McBuf)]
|
||||
pub struct UpdateGameMode {
|
||||
uuid: Uuid,
|
||||
pub uuid: Uuid,
|
||||
#[var]
|
||||
gamemode: u32,
|
||||
pub gamemode: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, McBuf)]
|
||||
pub struct UpdateLatency {
|
||||
uuid: Uuid,
|
||||
pub uuid: Uuid,
|
||||
#[var]
|
||||
ping: i32,
|
||||
pub ping: i32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, McBuf)]
|
||||
pub struct UpdateDisplayName {
|
||||
uuid: Uuid,
|
||||
display_name: Option<Component>,
|
||||
pub uuid: Uuid,
|
||||
pub display_name: Option<Component>,
|
||||
}
|
||||
#[derive(Clone, Debug, McBuf)]
|
||||
pub struct RemovePlayer {
|
||||
uuid: Uuid,
|
||||
pub uuid: Uuid,
|
||||
}
|
||||
|
||||
impl McBufReadable for Action {
|
||||
|
|
|
@ -9,7 +9,7 @@ pub struct ServerboundChatCommandPacket {
|
|||
pub command: String,
|
||||
// TODO: Choose a real timestamp type
|
||||
pub timestamp: u64,
|
||||
pub salt: i64,
|
||||
pub salt: u64,
|
||||
pub argument_signatures: Vec<ArgumentSignature>,
|
||||
pub signed_preview: bool,
|
||||
pub last_seen_messages: LastSeenMessagesUpdate,
|
||||
|
|
|
@ -3,20 +3,48 @@ use azalea_protocol_macros::ServerboundGamePacket;
|
|||
|
||||
#[derive(Clone, Debug, McBuf, ServerboundGamePacket)]
|
||||
pub struct ServerboundClientInformationPacket {
|
||||
/// The locale of the client.
|
||||
pub language: String,
|
||||
/// The view distance of the client in chunks, same as the render distance
|
||||
/// in-game.
|
||||
pub view_distance: u8,
|
||||
/// The types of chat messages the client wants to receive. Note that many
|
||||
/// servers ignore this.
|
||||
pub chat_visibility: ChatVisibility,
|
||||
/// Whether the messages sent from the server should have colors. Note that
|
||||
/// many servers ignore this and always send colored messages.
|
||||
pub chat_colors: bool,
|
||||
pub model_customisation: u8,
|
||||
pub main_hand: HumanoidArm,
|
||||
pub text_filtering_enabled: bool,
|
||||
/// Whether the client should show up as "Anonymous Player" in the server
|
||||
/// list.
|
||||
pub allows_listing: bool,
|
||||
}
|
||||
|
||||
impl Default for ServerboundClientInformationPacket {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
language: "en_us".to_string(),
|
||||
view_distance: 8,
|
||||
chat_visibility: ChatVisibility::Full,
|
||||
chat_colors: true,
|
||||
model_customisation: 0,
|
||||
main_hand: HumanoidArm::Right,
|
||||
text_filtering_enabled: false,
|
||||
allows_listing: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(McBuf, Clone, Copy, Debug)]
|
||||
pub enum ChatVisibility {
|
||||
/// All chat messages should be sent to the client.
|
||||
Full = 0,
|
||||
/// Chat messages from other players should be not sent to the client, only
|
||||
/// messages from the server like "Player joined the game" should be sent.
|
||||
System = 1,
|
||||
/// No chat messages should be sent to the client.
|
||||
Hidden = 2,
|
||||
}
|
||||
|
||||
|
|
|
@ -106,7 +106,7 @@ fn parse_frame(buffer: &mut BytesMut) -> Result<BytesMut, FrameSplitterError> {
|
|||
Ok(data)
|
||||
}
|
||||
|
||||
fn frame_splitter<'a>(buffer: &'a mut BytesMut) -> Result<Option<Vec<u8>>, FrameSplitterError> {
|
||||
fn frame_splitter(buffer: &mut BytesMut) -> Result<Option<Vec<u8>>, FrameSplitterError> {
|
||||
// https://tokio.rs/tokio/tutorial/framing
|
||||
let read_frame = parse_frame(buffer);
|
||||
match read_frame {
|
||||
|
@ -212,7 +212,7 @@ where
|
|||
|
||||
// if we were given a cipher, decrypt the packet
|
||||
if let Some(message) = framed.next().await {
|
||||
let mut bytes = message.unwrap();
|
||||
let mut bytes = message?;
|
||||
|
||||
if let Some(cipher) = cipher {
|
||||
azalea_crypto::decrypt_packet(cipher, &mut bytes);
|
||||
|
|
|
@ -29,7 +29,7 @@ fn packet_encoder<P: ProtocolPacket + std::fmt::Debug>(
|
|||
packet: &P,
|
||||
) -> Result<Vec<u8>, PacketEncodeError> {
|
||||
let mut buf = Vec::new();
|
||||
(packet.id() as u32).var_write_into(&mut buf)?;
|
||||
packet.id().var_write_into(&mut buf)?;
|
||||
packet.write(&mut buf)?;
|
||||
if buf.len() > MAXIMUM_UNCOMPRESSED_LENGTH as usize {
|
||||
return Err(PacketEncodeError::TooBig {
|
||||
|
|
|
@ -3,10 +3,10 @@ description = "Use Minecraft's registries."
|
|||
edition = "2021"
|
||||
license = "MIT"
|
||||
name = "azalea-registry"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
azalea-buf = {path = "../azalea-buf", version = "^0.1.0"}
|
||||
azalea-registry-macros = {path = "./azalea-registry-macros", version = "^0.1.0"}
|
||||
azalea-buf = {path = "../azalea-buf", version = "^0.2.0" }
|
||||
azalea-registry-macros = {path = "./azalea-registry-macros", version = "^0.2.0" }
|
||||
|
|
|
@ -3,7 +3,7 @@ description = "Macros internally used in azalea-registry."
|
|||
edition = "2021"
|
||||
license = "MIT"
|
||||
name = "azalea-registry-macros"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
|
|
@ -3,17 +3,17 @@ description = "The Minecraft world representation used in Azalea."
|
|||
edition = "2021"
|
||||
license = "MIT"
|
||||
name = "azalea-world"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
azalea-block = {path = "../azalea-block", default-features = false, version = "^0.1.0"}
|
||||
azalea-buf = {path = "../azalea-buf", version = "^0.1.0"}
|
||||
azalea-chat = {path = "../azalea-chat", version = "^0.1.0"}
|
||||
azalea-core = {path = "../azalea-core", version = "^0.1.0"}
|
||||
azalea-nbt = {path = "../azalea-nbt", version = "^0.1.0"}
|
||||
azalea-registry = {path = "../azalea-registry", version = "^0.1.0"}
|
||||
azalea-block = {path = "../azalea-block", default-features = false, version = "^0.2.0" }
|
||||
azalea-buf = {path = "../azalea-buf", version = "^0.2.0" }
|
||||
azalea-chat = {path = "../azalea-chat", version = "^0.2.0" }
|
||||
azalea-core = {path = "../azalea-core", version = "^0.2.0" }
|
||||
azalea-nbt = {path = "../azalea-nbt", version = "^0.2.0" }
|
||||
azalea-registry = {path = "../azalea-registry", version = "^0.2.0" }
|
||||
log = "0.4.17"
|
||||
nohash-hasher = "0.2.0"
|
||||
thiserror = "1.0.34"
|
||||
|
|
|
@ -120,21 +120,21 @@ impl BitStorage {
|
|||
|
||||
let values_per_long = 64 / bits;
|
||||
let magic_index = values_per_long - 1;
|
||||
let (divide_mul, divide_add, divide_shift) = MAGIC[magic_index as usize];
|
||||
let (divide_mul, divide_add, divide_shift) = MAGIC[magic_index];
|
||||
let calculated_length = (size + values_per_long - 1) / values_per_long;
|
||||
|
||||
let mask = (1 << bits) - 1;
|
||||
|
||||
let using_data = if let Some(data) = data {
|
||||
if data.len() != calculated_length as usize {
|
||||
if data.len() != calculated_length {
|
||||
return Err(BitStorageError::InvalidLength {
|
||||
got: data.len(),
|
||||
expected: calculated_length as usize,
|
||||
expected: calculated_length,
|
||||
});
|
||||
}
|
||||
data
|
||||
} else {
|
||||
vec![0; calculated_length as usize]
|
||||
vec![0; calculated_length]
|
||||
};
|
||||
|
||||
Ok(BitStorage {
|
||||
|
@ -179,7 +179,7 @@ impl BitStorage {
|
|||
}
|
||||
|
||||
let cell_index = self.cell_index(index as u64);
|
||||
let cell = &self.data[cell_index as usize];
|
||||
let cell = &self.data[cell_index];
|
||||
let bit_index = (index - cell_index * self.values_per_long as usize) * self.bits;
|
||||
cell >> bit_index & self.mask
|
||||
}
|
||||
|
@ -193,7 +193,7 @@ impl BitStorage {
|
|||
assert!(index < self.size);
|
||||
assert!(value <= self.mask);
|
||||
let cell_index = self.cell_index(index as u64);
|
||||
let cell = &mut self.data[cell_index as usize];
|
||||
let cell = &mut self.data[cell_index];
|
||||
let bit_index = (index - cell_index * self.values_per_long as usize) * self.bits;
|
||||
let old_value = *cell >> (bit_index as u64) & self.mask;
|
||||
*cell = *cell & !(self.mask << bit_index) | (value & self.mask) << bit_index;
|
||||
|
@ -209,7 +209,7 @@ impl BitStorage {
|
|||
assert!(index < self.size);
|
||||
assert!(value <= self.mask);
|
||||
let cell_index = self.cell_index(index as u64);
|
||||
let cell = &mut self.data[cell_index as usize];
|
||||
let cell = &mut self.data[cell_index];
|
||||
let bit_index = (index - cell_index * self.values_per_long as usize) * self.bits;
|
||||
*cell = *cell & !(self.mask << bit_index) | (value & self.mask) << bit_index;
|
||||
}
|
||||
|
|
|
@ -104,9 +104,10 @@ impl ChunkStorage {
|
|||
data: &mut Cursor<&[u8]>,
|
||||
) -> Result<(), BufReadError> {
|
||||
if !self.in_range(pos) {
|
||||
println!(
|
||||
log::trace!(
|
||||
"Ignoring chunk since it's not in the view range: {}, {}",
|
||||
pos.x, pos.z
|
||||
pos.x,
|
||||
pos.z
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
@ -115,7 +116,8 @@ impl ChunkStorage {
|
|||
data,
|
||||
self.height,
|
||||
)?));
|
||||
println!("Loaded chunk {:?}", pos);
|
||||
|
||||
log::trace!("Loaded chunk {:?}", pos);
|
||||
self[pos] = Some(chunk);
|
||||
|
||||
Ok(())
|
||||
|
|
|
@ -110,7 +110,7 @@ impl McBufReadable for EntityDataValue {
|
|||
if val == 0 {
|
||||
None
|
||||
} else {
|
||||
Some((val - 1) as u32)
|
||||
Some(val - 1)
|
||||
}
|
||||
}),
|
||||
18 => EntityDataValue::Pose(Pose::read_from(buf)?),
|
||||
|
|
|
@ -121,9 +121,8 @@ impl PalettedContainer {
|
|||
}
|
||||
|
||||
fn on_resize(&mut self, bits_per_entry: u8, value: u32) -> usize {
|
||||
if bits_per_entry > 5 {
|
||||
panic!("bits_per_entry must be <= 5");
|
||||
}
|
||||
// in vanilla this is always true, but it's sometimes false in purpur servers
|
||||
// assert!(bits_per_entry <= 5, "bits_per_entry must be <= 5");
|
||||
let mut new_data = self.create_or_reuse_data(bits_per_entry);
|
||||
new_data.copy_from(&self.palette, &self.storage);
|
||||
*self = new_data;
|
||||
|
@ -149,7 +148,7 @@ impl PalettedContainer {
|
|||
}
|
||||
Palette::Linear(palette) => {
|
||||
if let Some(index) = palette.iter().position(|v| *v == value) {
|
||||
return index as usize;
|
||||
return index;
|
||||
}
|
||||
let capacity = 2usize.pow(self.bits_per_entry.into());
|
||||
if capacity > palette.len() {
|
||||
|
@ -162,7 +161,7 @@ impl PalettedContainer {
|
|||
Palette::Hashmap(palette) => {
|
||||
// TODO? vanilla keeps this in memory as a hashmap, but also i don't care
|
||||
if let Some(index) = palette.iter().position(|v| *v == value) {
|
||||
return index as usize;
|
||||
return index;
|
||||
}
|
||||
let capacity = 2usize.pow(self.bits_per_entry.into());
|
||||
if capacity > palette.len() {
|
||||
|
|
|
@ -1,18 +1,17 @@
|
|||
[package]
|
||||
description = "Advertisement crate for Azalea."
|
||||
description = "A framework for creating Minecraft bots."
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
|
||||
name = "azalea"
|
||||
version = "0.1.0"
|
||||
version = "0.2.4"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
anyhow = "^1.0.65"
|
||||
async-trait = "^0.1.57"
|
||||
azalea-client = {version = "0.1.0", path = "../azalea-client"}
|
||||
azalea-protocol = {version = "0.1.0", path = "../azalea-protocol"}
|
||||
azalea-client = { version = "0.2.2", path = "../azalea-client" }
|
||||
azalea-protocol = { version = "0.2.0", path = "../azalea-protocol" }
|
||||
parking_lot = "^0.12.1"
|
||||
thiserror = "^1.0.37"
|
||||
tokio = "^1.21.1"
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
A framework for creating Minecraft bots.
|
||||
Azalea is a framework for creating Minecraft bots.
|
||||
|
||||
Interally, it's just a wrapper over azalea-client, adding useful functions for making bots.
|
||||
Internally, it's just a wrapper over azalea-client, adding useful functions for making bots.
|
||||
|
|
|
@ -1,22 +1,22 @@
|
|||
use azalea::{pathfinder, Account};
|
||||
use azalea::{Bot, Client, Event};
|
||||
use azalea::pathfinder;
|
||||
use azalea::prelude::*;
|
||||
use parking_lot::Mutex;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Default)]
|
||||
#[derive(Default, Clone)]
|
||||
struct State {
|
||||
pub started: bool,
|
||||
pub started: Arc<Mutex<bool>>,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let account = Account::offline("bot");
|
||||
// or let bot = azalea::Bot::microsoft("access token").await;
|
||||
// or let bot = Account::microsoft("email").await;
|
||||
|
||||
azalea::start(azalea::Options {
|
||||
account,
|
||||
address: "localhost",
|
||||
state: Arc::new(Mutex::new(State::default())),
|
||||
state: State::default(),
|
||||
plugins: vec![],
|
||||
handle,
|
||||
})
|
||||
|
@ -24,13 +24,13 @@ async fn main() {
|
|||
.unwrap();
|
||||
}
|
||||
|
||||
async fn handle(bot: Client, event: Arc<Event>, state: Arc<Mutex<State>>) {
|
||||
async fn handle(bot: Client, event: Event, state: State) -> anyhow::Result<()> {
|
||||
match event {
|
||||
Event::Message(m) => {
|
||||
Event::Chat(m) => {
|
||||
if m.username == bot.player.username {
|
||||
return;
|
||||
return Ok(());
|
||||
};
|
||||
if m.message = "go" {
|
||||
if m.content == "go" {
|
||||
// make sure we only start once
|
||||
let ctx_lock = ctx.lock().unwrap();
|
||||
if ctx_lock.started {
|
||||
|
@ -74,4 +74,6 @@ async fn handle(bot: Client, event: Arc<Event>, state: Arc<Mutex<State>>) {
|
|||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -1,17 +1,16 @@
|
|||
use std::sync::Arc;
|
||||
//! A simple bot that repeats chat messages sent by other players.
|
||||
|
||||
use azalea::{Account, Client, Event};
|
||||
use parking_lot::Mutex;
|
||||
use azalea::prelude::*;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let account = Account::offline("bot");
|
||||
// or let account = azalea::Account::microsoft("access token").await;
|
||||
// or let account = Account::microsoft("email").await;
|
||||
|
||||
azalea::start(azalea::Options {
|
||||
account,
|
||||
address: "localhost",
|
||||
state: Arc::new(Mutex::new(State::default())),
|
||||
state: State::default(),
|
||||
plugins: vec![],
|
||||
handle,
|
||||
})
|
||||
|
@ -19,25 +18,16 @@ async fn main() {
|
|||
.unwrap();
|
||||
}
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
pub struct State {}
|
||||
|
||||
async fn handle(bot: Client, event: Arc<Event>, state: Arc<Mutex<State>>) -> anyhow::Result<()> {
|
||||
async fn handle(bot: Client, event: Event, state: State) -> anyhow::Result<()> {
|
||||
match event {
|
||||
Event::Chat(m) => {
|
||||
if m.username == bot.username {
|
||||
return Ok(()); // ignore our own messages
|
||||
};
|
||||
bot.chat(m.message).await;
|
||||
}
|
||||
Event::Kick(m) => {
|
||||
println!(m);
|
||||
bot.reconnect().await.unwrap();
|
||||
}
|
||||
Event::HungerUpdate(h) => {
|
||||
if !h.using_held_item() && h.hunger <= 17 {
|
||||
bot.hold(azalea::ItemGroup::Food).await?;
|
||||
bot.use_held_item().await?;
|
||||
}
|
||||
bot.chat(m.content).await;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use azalea::{pathfinder, Account, Accounts, Client, Event};
|
||||
use azalea::{pathfinder, Account, Accounts, Client, Event, Swarm};
|
||||
use parking_lot::Mutex;
|
||||
use std::sync::Arc;
|
||||
|
||||
|
@ -7,60 +7,53 @@ async fn main() {
|
|||
let accounts = Accounts::new();
|
||||
|
||||
for i in 0..10 {
|
||||
accounts.add(Account::offline(format!("bot{}", i)));
|
||||
accounts.add(Account::offline(&format!("bot{}", i)));
|
||||
}
|
||||
|
||||
azalea::start_group(azalea::GroupOptions {
|
||||
azalea::start_swarm(azalea::SwarmOptions {
|
||||
accounts,
|
||||
address: "localhost",
|
||||
|
||||
group_state: Arc::new(Mutex::new(State::default())),
|
||||
swarm_state: State::default(),
|
||||
state: State::default(),
|
||||
|
||||
group_plugins: vec![Arc::new(pathfinder::Plugin::default())],
|
||||
swarm_plugins: vec![Arc::new(pathfinder::Plugin::default())],
|
||||
plugins: vec![],
|
||||
|
||||
handle: Box::new(handle),
|
||||
group_handle: Box::new(handle),
|
||||
swarm_handle: Box::new(swarm_handle),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
#[derive(Default, Clone)]
|
||||
struct State {}
|
||||
|
||||
#[derive(Default)]
|
||||
struct GroupState {}
|
||||
|
||||
async fn handle(bot: Client, event: Arc<Event>, state: Arc<Mutex<State>>) -> anyhow::Result<()> {
|
||||
match event {
|
||||
_ => {}
|
||||
}
|
||||
#[derive(Default, Clone)]
|
||||
struct SwarmState {}
|
||||
|
||||
async fn handle(bot: Client, event: Event, state: State) -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn group_handle(
|
||||
bots: Swarm,
|
||||
event: Arc<Event>,
|
||||
state: Arc<Mutex<GroupState>>,
|
||||
) -> anyhow::Result<()> {
|
||||
match *event {
|
||||
async fn swarm_handle(swarm: Swarm, event: Event, state: SwarmState) -> anyhow::Result<()> {
|
||||
match event {
|
||||
Event::Login => {
|
||||
bots.goto(azalea::BlockPos::new(0, 70, 0)).await;
|
||||
swarm.goto(azalea::BlockPos::new(0, 70, 0)).await;
|
||||
// or bots.goto_goal(pathfinder::Goals::Goto(azalea::BlockPos(0, 70, 0))).await;
|
||||
|
||||
// destroy the blocks in this area and then leave
|
||||
|
||||
bots.fill(
|
||||
azalea::Selection::Range(
|
||||
azalea::BlockPos::new(0, 0, 0),
|
||||
azalea::BlockPos::new(16, 255, 16),
|
||||
),
|
||||
azalea::block::Air,
|
||||
)
|
||||
.await;
|
||||
swarm
|
||||
.fill(
|
||||
azalea::Selection::Range(
|
||||
azalea::BlockPos::new(0, 0, 0),
|
||||
azalea::BlockPos::new(16, 255, 16),
|
||||
),
|
||||
azalea::block::Air,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
|
|
@ -4,17 +4,17 @@ use async_trait::async_trait;
|
|||
use azalea::{Client, Event};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
#[derive(Default)]
|
||||
#[derive(Default, Clone)]
|
||||
pub struct Plugin {
|
||||
pub state: Arc<Mutex<State>>,
|
||||
pub state: State,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
#[derive(Default, Clone)]
|
||||
pub struct State {}
|
||||
|
||||
#[async_trait]
|
||||
impl azalea::Plugin for Plugin {
|
||||
async fn handle(self: Arc<Self>, bot: Client, event: Arc<Event>) {
|
||||
async fn handle(self: Box<Self>, event: Event, bot: Client) {
|
||||
match event {
|
||||
Event::UpdateHunger => {
|
||||
if !bot.using_held_item() && bot.food_level() <= 17 {
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
mod autoeat;
|
||||
|
||||
use azalea::prelude::*;
|
||||
use azalea::{pathfinder, Account, BlockPos, Client, Event, ItemKind, MoveDirection, Plugin, Vec3};
|
||||
use parking_lot::Mutex;
|
||||
use std::sync::Arc;
|
||||
use azalea::{pathfinder, BlockPos, ItemKind, Vec3};
|
||||
|
||||
#[derive(Default)]
|
||||
#[derive(Default, Clone)]
|
||||
struct State {}
|
||||
|
||||
#[tokio::main]
|
||||
|
@ -17,10 +15,10 @@ async fn main() {
|
|||
azalea::start(azalea::Options {
|
||||
account,
|
||||
address: "localhost",
|
||||
state: Arc::new(Mutex::new(State::default())),
|
||||
state: State::default(),
|
||||
plugins: vec![
|
||||
Arc::new(autoeat::Plugin::default()),
|
||||
Arc::new(pathfinder::Plugin::default()),
|
||||
Box::new(autoeat::Plugin::default()),
|
||||
Box::new(pathfinder::Plugin::default()),
|
||||
],
|
||||
handle,
|
||||
})
|
||||
|
@ -28,7 +26,7 @@ async fn main() {
|
|||
.unwrap();
|
||||
}
|
||||
|
||||
async fn handle(bot: Client, event: Arc<Event>, state: Arc<Mutex<State>>) -> anyhow::Result<()> {
|
||||
async fn handle(bot: Client, event: Event, state: State) -> anyhow::Result<()> {
|
||||
match event {
|
||||
Event::Login => {
|
||||
goto_farm(bot, state).await?;
|
||||
|
@ -42,14 +40,14 @@ async fn handle(bot: Client, event: Arc<Event>, state: Arc<Mutex<State>>) -> any
|
|||
}
|
||||
|
||||
// go to the place where we start farming
|
||||
async fn goto_farm(bot: Client, state: Arc<Mutex<State>>) -> anyhow::Result<()> {
|
||||
async fn goto_farm(bot: Client, state: State) -> anyhow::Result<()> {
|
||||
bot.goto(pathfinder::Goals::Near(5, BlockPos::new(0, 70, 0)))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// go to the chest and deposit everything in our inventory.
|
||||
async fn deposit(bot: &mut Client, state: &mut Arc<Mutex<State>>) -> anyhow::Result<()> {
|
||||
async fn deposit(bot: &mut Client, state: State) -> anyhow::Result<()> {
|
||||
// first throw away any garbage we might have
|
||||
bot.toss(|item| item.kind != ItemKind::Potato && item.kind != ItemKind::DiamondHoe);
|
||||
|
||||
|
|
|
@ -1,51 +1,52 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use azalea::{pathfinder, Account, Accounts, Client, Event};
|
||||
use parking_lot::Mutex;
|
||||
use azalea::{pathfinder, Account, Client, Event};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let accounts = Accounts::new();
|
||||
let accounts = Vec::new();
|
||||
|
||||
for i in 0..10 {
|
||||
accounts.add(Account::offline(format!("bot{}", i)));
|
||||
accounts.push(Account::offline(&format!("bot{}", i)));
|
||||
}
|
||||
|
||||
azalea::start_swarm(azalea::SwarmOptions {
|
||||
accounts,
|
||||
address: "localhost",
|
||||
|
||||
swarm_state: Arc::new(Mutex::new(State::default())),
|
||||
swarm_state: State::default(),
|
||||
state: State::default(),
|
||||
|
||||
swarm_plugins: vec![Arc::new(pathfinder::Plugin::default())],
|
||||
swarm_plugins: vec![Box::new(pathfinder::Plugin::default())],
|
||||
plugins: vec![],
|
||||
|
||||
handle: Box::new(handle),
|
||||
swarm_handle: Box::new(handle),
|
||||
swarm_handle: Box::new(swarm_handle),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
struct State {}
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
struct SwarmState {}
|
||||
|
||||
async fn handle(bots: Client, event: Arc<Event>, state: Arc<Mutex<State>>) {
|
||||
match *event {
|
||||
async fn handle(bot: Client, event: Event, state: State) {}
|
||||
async fn swarm_handle(swarm: Swarm, event: Event, state: State) {
|
||||
match event {
|
||||
Event::Tick => {
|
||||
// choose an arbitrary player within render distance to target
|
||||
if let Some(target) = bots
|
||||
.dimension()
|
||||
if let Some(target) = swarm
|
||||
.dimension
|
||||
.find_one_entity(|e| e.id == "minecraft:player")
|
||||
{
|
||||
for bot in bots {
|
||||
for bot in swarm {
|
||||
bot.tick_goto_goal(pathfinder::Goals::Reach(target.bounding_box));
|
||||
// if target.bounding_box.distance(bot.eyes) < bot.reach_distance() {
|
||||
if bot.entity.can_reach(target.bounding_box) {
|
||||
bot.swing();
|
||||
}
|
||||
if !h.using_held_item() && bot.state.lock().hunger <= 17 {
|
||||
if !bot.using_held_item() && bot.state.lock().hunger <= 17 {
|
||||
bot.hold(azalea::ItemGroup::Food);
|
||||
tokio::task::spawn(bot.use_held_item());
|
||||
}
|
||||
|
|
|
@ -3,14 +3,14 @@ use async_trait::async_trait;
|
|||
use parking_lot::Mutex;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Default)]
|
||||
#[derive(Default, Clone)]
|
||||
pub struct Plugin {
|
||||
pub state: Arc<Mutex<State>>,
|
||||
pub state: State,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
#[derive(Default, Clone)]
|
||||
pub struct State {
|
||||
jumping_once: bool,
|
||||
jumping_once: Arc<Mutex<bool>>,
|
||||
}
|
||||
|
||||
pub trait BotTrait {
|
||||
|
@ -18,7 +18,7 @@ pub trait BotTrait {
|
|||
}
|
||||
|
||||
impl BotTrait for azalea_client::Client {
|
||||
/// Try to jump next tick.
|
||||
/// Queue a jump for the next tick.
|
||||
fn jump(&self) {
|
||||
let player_lock = self.player.lock();
|
||||
let mut dimension_lock = self.dimension.lock();
|
||||
|
@ -33,12 +33,11 @@ impl BotTrait for azalea_client::Client {
|
|||
|
||||
#[async_trait]
|
||||
impl crate::Plugin for Plugin {
|
||||
async fn handle(self: Arc<Self>, mut bot: Client, event: Arc<Event>) {
|
||||
if let Event::Tick = *event {
|
||||
let mut state = self.state.lock();
|
||||
if state.jumping_once {
|
||||
async fn handle(self: Box<Self>, event: Event, mut bot: Client) {
|
||||
if let Event::Tick = event {
|
||||
if *self.state.jumping_once.lock() {
|
||||
if bot.jumping() {
|
||||
state.jumping_once = false;
|
||||
*self.state.jumping_once.lock() = false;
|
||||
} else {
|
||||
bot.set_jumping(true);
|
||||
}
|
||||
|
|
|
@ -1,31 +1,121 @@
|
|||
//! Azalea is a framework for creating Minecraft bots.
|
||||
//!
|
||||
//! Internally, it's just a wrapper over [`azalea_client`], adding useful
|
||||
//! functions for making bots. Because of this, lots of the documentation will
|
||||
//! refer to `azalea_client`. You can just replace these with `azalea` in your
|
||||
//! code, since everything from azalea_client is re-exported in azalea.
|
||||
//!
|
||||
//! # Examples
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! //! A bot that logs chat messages sent in the server to the console.
|
||||
//!
|
||||
//! use azalea::prelude::*;
|
||||
//! use parking_lot::Mutex;
|
||||
//! use std::sync::Arc;
|
||||
//!
|
||||
//! #[tokio::main]
|
||||
//! async fn main() {
|
||||
//! let account = Account::offline("bot");
|
||||
//! // or Account::microsoft("example@example.com").await.unwrap();
|
||||
//!
|
||||
//! azalea::start(azalea::Options {
|
||||
//! account,
|
||||
//! address: "localhost",
|
||||
//! state: State::default(),
|
||||
//! plugins: vec![],
|
||||
//! handle,
|
||||
//! })
|
||||
//! .await
|
||||
//! .unwrap();
|
||||
//! }
|
||||
//!
|
||||
//! #[derive(Default, Clone)]
|
||||
//! pub struct State {}
|
||||
//!
|
||||
//! async fn handle(bot: Client, event: Event, state: State) -> anyhow::Result<()> {
|
||||
//! match event {
|
||||
//! Event::Chat(m) => {
|
||||
//! println!(m.message().to_ansi(None));
|
||||
//! }
|
||||
//! _ => {}
|
||||
//! }
|
||||
//!
|
||||
//! Ok(())
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! [`azalea_client`]: https://crates.io/crates/azalea-client
|
||||
|
||||
mod bot;
|
||||
pub mod prelude;
|
||||
|
||||
use async_trait::async_trait;
|
||||
pub use azalea_client::*;
|
||||
use azalea_protocol::ServerAddress;
|
||||
use parking_lot::Mutex;
|
||||
use std::{future::Future, sync::Arc};
|
||||
use std::future::Future;
|
||||
use thiserror::Error;
|
||||
|
||||
/// Plugins can keep their own personal state, listen to events, and add new functions to Client.
|
||||
#[async_trait]
|
||||
pub trait Plugin: Send + Sync {
|
||||
async fn handle(self: Arc<Self>, bot: Client, event: Arc<Event>);
|
||||
pub trait Plugin: Send + Sync + PluginClone + 'static {
|
||||
async fn handle(self: Box<Self>, event: Event, bot: Client);
|
||||
}
|
||||
|
||||
// pub type HeuristicFn<N, W> = fn(start: &Vertex<N, W>, current: &Vertex<N, W>) -> W;
|
||||
pub type HandleFn<Fut, S> = fn(Client, Arc<Event>, Arc<Mutex<S>>) -> Fut;
|
||||
/// An internal trait that allows Plugin to be cloned.
|
||||
#[doc(hidden)]
|
||||
pub trait PluginClone {
|
||||
fn clone_box(&self) -> Box<dyn Plugin>;
|
||||
}
|
||||
impl<T> PluginClone for T
|
||||
where
|
||||
T: 'static + Plugin + Clone,
|
||||
{
|
||||
fn clone_box(&self) -> Box<dyn Plugin> {
|
||||
Box::new(self.clone())
|
||||
}
|
||||
}
|
||||
impl Clone for Box<dyn Plugin> {
|
||||
fn clone(&self) -> Self {
|
||||
self.clone_box()
|
||||
}
|
||||
}
|
||||
|
||||
pub type HandleFn<Fut, S> = fn(Client, Event, S) -> Fut;
|
||||
|
||||
/// The options that are passed to [`azalea::start`].
|
||||
///
|
||||
/// [`azalea::start`]: fn.start.html
|
||||
pub struct Options<S, A, Fut>
|
||||
where
|
||||
A: TryInto<ServerAddress>,
|
||||
Fut: Future<Output = Result<(), anyhow::Error>>,
|
||||
{
|
||||
/// The address of the server that we're connecting to. This can be a
|
||||
/// `&str`, [`ServerAddress`], or anything that implements
|
||||
/// `TryInto<ServerAddress>`.
|
||||
pub address: A,
|
||||
/// The account that's going to join the server,
|
||||
pub account: Account,
|
||||
pub plugins: Vec<Arc<dyn Plugin>>,
|
||||
pub state: Arc<Mutex<S>>,
|
||||
/// A list of plugins that are going to be used. Plugins are external
|
||||
/// crates that add extra functionality to Azalea.
|
||||
pub plugins: Vec<Box<dyn Plugin>>,
|
||||
/// A struct that contains the data that you want your bot to remember
|
||||
/// across events.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use parking_lot::Mutex;
|
||||
/// use std::sync::Arc;
|
||||
///
|
||||
/// #[derive(Default, Clone)]
|
||||
/// struct State {
|
||||
/// farming: Arc<Mutex<bool>>,
|
||||
/// }
|
||||
/// ```
|
||||
pub state: S,
|
||||
/// The function that's called whenever we get an event.
|
||||
pub handle: HandleFn<Fut, S>,
|
||||
}
|
||||
|
||||
|
@ -35,19 +125,22 @@ pub enum Error {
|
|||
InvalidAddress,
|
||||
}
|
||||
|
||||
/// Join a Minecraft server.
|
||||
/// Join a server and start handling events. This function will run forever until
|
||||
/// it gets disconnected from the server.
|
||||
///
|
||||
/// ```no_run
|
||||
/// azalea::start(azalea::Options {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// let error = azalea::start(azalea::Options {
|
||||
/// account,
|
||||
/// address: "localhost",
|
||||
/// state: Arc::new(Mutex::new(State::default())),
|
||||
/// plugins: vec![&autoeat::Plugin::default()],
|
||||
/// handle: Box::new(handle),
|
||||
/// }).await.unwrap();
|
||||
/// state: State::default(),
|
||||
/// plugins: vec![Box::new(autoeat::Plugin::default())],
|
||||
/// handle,
|
||||
/// }).await;
|
||||
/// ```
|
||||
pub async fn start<
|
||||
S: Send + 'static,
|
||||
S: Send + Sync + Clone + 'static,
|
||||
A: Send + TryInto<ServerAddress>,
|
||||
Fut: Future<Output = Result<(), anyhow::Error>> + Send + 'static,
|
||||
>(
|
||||
|
@ -58,25 +151,22 @@ pub async fn start<
|
|||
Err(_) => return Err(Error::InvalidAddress),
|
||||
};
|
||||
|
||||
let (bot, mut rx) = options.account.join(&address).await.unwrap();
|
||||
let (bot, mut rx) = Client::join(&options.account, address).await.unwrap();
|
||||
|
||||
let state = options.state;
|
||||
let bot_plugin = Arc::new(bot::Plugin::default());
|
||||
let bot_plugin = bot::Plugin::default();
|
||||
|
||||
while let Some(event) = rx.recv().await {
|
||||
// we put it into an Arc so it's cheaper to clone
|
||||
let event = Arc::new(event);
|
||||
|
||||
for plugin in &options.plugins {
|
||||
tokio::spawn(plugin.clone().handle(bot.clone(), event.clone()));
|
||||
let plugin = plugin.clone();
|
||||
tokio::spawn(plugin.handle(event.clone(), bot.clone()));
|
||||
}
|
||||
|
||||
{
|
||||
let bot_plugin = bot_plugin.clone();
|
||||
let bot = bot.clone();
|
||||
let event = event.clone();
|
||||
tokio::spawn(bot::Plugin::handle(bot_plugin, bot, event));
|
||||
};
|
||||
tokio::spawn(bot::Plugin::handle(
|
||||
Box::new(bot_plugin.clone()),
|
||||
event.clone(),
|
||||
bot.clone(),
|
||||
));
|
||||
tokio::spawn((options.handle)(bot.clone(), event.clone(), state.clone()));
|
||||
}
|
||||
|
||||
|
|
|
@ -1 +1,4 @@
|
|||
//! The Azalea prelude. Things that are necessary for a bare-bones bot are re-exported here.
|
||||
|
||||
pub use crate::bot::BotTrait;
|
||||
pub use azalea_client::{Account, Client, Event};
|
||||
|
|
4
bot/Cargo.toml
Executable file → Normal file
4
bot/Cargo.toml
Executable file → Normal file
|
@ -1,7 +1,9 @@
|
|||
[package]
|
||||
edition = "2021"
|
||||
name = "bot"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
publish = false
|
||||
release = false
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
|
|
|
@ -1,31 +1,40 @@
|
|||
use azalea::prelude::*;
|
||||
use azalea::{Account, Client, Event};
|
||||
use parking_lot::Mutex;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Default)]
|
||||
#[derive(Default, Clone)]
|
||||
struct State {}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
env_logger::init();
|
||||
|
||||
let account = Account::offline("bot");
|
||||
let account = Account::microsoft("example@example.com").await?;
|
||||
|
||||
azalea::start(azalea::Options {
|
||||
account,
|
||||
address: "localhost",
|
||||
state: Arc::new(Mutex::new(State::default())),
|
||||
state: State::default(),
|
||||
plugins: vec![],
|
||||
handle,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle(bot: Client, event: Arc<Event>, _state: Arc<Mutex<State>>) -> anyhow::Result<()> {
|
||||
if let Event::Tick = *event {
|
||||
bot.jump();
|
||||
async fn handle(bot: Client, event: Event, _state: State) -> anyhow::Result<()> {
|
||||
match event {
|
||||
Event::Login => {
|
||||
bot.chat("Hello world").await?;
|
||||
}
|
||||
Event::Initialize => {
|
||||
println!("initialized");
|
||||
}
|
||||
Event::Tick => {
|
||||
bot.jump();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
|
@ -123,13 +123,17 @@ def get_generator_mod_data(version_id: str, category: str):
|
|||
with open(get_dir_location(f'{generator_mod_dir}/src/main/resources/fabric.mod.json'), 'w') as f:
|
||||
json.dump(fabric_mod_json, f, indent=2)
|
||||
|
||||
try: os.system(f'cd {generator_mod_dir} && chmod u+x ./gradlew')
|
||||
except: pass
|
||||
try:
|
||||
os.system(f'cd {generator_mod_dir} && chmod u+x ./gradlew')
|
||||
except:
|
||||
pass
|
||||
|
||||
# set the server port to something other than 25565 so it doesn't
|
||||
# conflict with anything else that's running
|
||||
try: os.makedirs(get_dir_location(f'{generator_mod_dir}/run'))
|
||||
except: pass
|
||||
try:
|
||||
os.makedirs(get_dir_location(f'{generator_mod_dir}/run'))
|
||||
except:
|
||||
pass
|
||||
with open(get_dir_location(f'{generator_mod_dir}/run/server.properties'), 'w') as f:
|
||||
f.write('server-port=56553')
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue