1
2
Fork 0
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:
mat 2022-10-23 22:10:13 -05:00
commit 2d3031517f
76 changed files with 2123 additions and 422 deletions

4
.gitignore vendored
View file

@ -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
View file

@ -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",
]

View file

@ -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
View 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"] }

View file

@ -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).

View 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
View 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
View 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
}

View file

@ -1,3 +1,6 @@
//! Handle Minecraft authentication.
mod auth;
mod cache;
pub mod game_profile;
pub mod sessionserver;
pub use auth::*;

View 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,
})
}
}
}

View file

@ -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" }

View file

@ -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
View 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

View file

@ -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;

View file

@ -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"

View file

@ -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

View file

@ -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>);

View file

@ -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);
}
}

View file

@ -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,

View file

@ -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
View 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"

View file

@ -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(())
}
}

View file

@ -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
View 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
View 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
View 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"

View file

@ -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
View 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()
}

View file

@ -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 {

View 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
}
}

View file

@ -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;

View file

@ -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);

View file

@ -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
View 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"

View file

@ -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,

View file

@ -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"]}

View file

@ -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()
}

View file

@ -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()
}

View file

@ -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
View 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"

View file

@ -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>()?);
}

View file

@ -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]

View file

@ -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));
}
}
}

View file

@ -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 {

View file

@ -1,5 +1,4 @@
#![feature(trait_alias)]
#![feature(let_chains)]
pub mod collision;

24
azalea-protocol/Cargo.toml Executable file → Normal file
View 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"

View file

@ -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.

View file

@ -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

View file

@ -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

View file

@ -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>,

View file

@ -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 {

View file

@ -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,

View file

@ -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,
}

View file

@ -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);

View file

@ -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 {

View file

@ -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" }

View file

@ -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

View file

@ -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"

View file

@ -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;
}

View file

@ -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(())

View file

@ -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)?),

View file

@ -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() {

View file

@ -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"

View file

@ -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.

View file

@ -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(())
}

View file

@ -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;
}
_ => {}
}

View file

@ -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;
}
_ => {}
}

View file

@ -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 {

View file

@ -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);

View file

@ -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());
}

View file

@ -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);
}

View file

@ -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()));
}

View file

@ -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
View 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

View file

@ -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(())

View file

@ -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')