diff --git a/Cargo.lock b/Cargo.lock
index 1697dbb9..b663ca49 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -69,9 +69,9 @@ checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
[[package]]
name = "anstyle"
-version = "1.0.1"
+version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3a30da5c5f2d5e72842e00bcb57657162cdabef0931f40e2deb9b4140440cecd"
+checksum = "b84bf0a05bbb2a83e5eb6fa36bb6e87baa08193c35ff52bbf6b38d8af2890e46"
[[package]]
name = "anyhow"
@@ -92,9 +92,9 @@ dependencies = [
[[package]]
name = "async-compression"
-version = "0.4.2"
+version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d495b6dc0184693324491a5ac05f559acc97bf937ab31d7a1c33dd0016be6d2b"
+checksum = "bb42b2197bf15ccb092b62c74515dbd8b86d0effd934795f6687c93b6e679a2c"
dependencies = [
"flate2",
"futures-core",
@@ -134,7 +134,7 @@ checksum = "5fd55a5ba1179988837d24ab4c7cc8ed6efdeff578ede0416b4225a5fca35bd0"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.31",
+ "syn 2.0.33",
]
[[package]]
@@ -151,7 +151,7 @@ checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.31",
+ "syn 2.0.33",
]
[[package]]
@@ -232,7 +232,7 @@ version = "0.8.0"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.31",
+ "syn 2.0.33",
]
[[package]]
@@ -262,7 +262,7 @@ version = "0.8.0"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.31",
+ "syn 2.0.33",
]
[[package]]
@@ -309,6 +309,8 @@ dependencies = [
"parking_lot",
"regex",
"reqwest",
+ "serde",
+ "serde_json",
"thiserror",
"tokio",
"uuid",
@@ -384,7 +386,7 @@ version = "0.8.0"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.31",
+ "syn 2.0.33",
]
[[package]]
@@ -479,7 +481,7 @@ version = "0.8.0"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.31",
+ "syn 2.0.33",
]
[[package]]
@@ -498,7 +500,7 @@ version = "0.8.0"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.31",
+ "syn 2.0.33",
]
[[package]]
@@ -540,9 +542,9 @@ dependencies = [
[[package]]
name = "base64"
-version = "0.21.3"
+version = "0.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "414dcefbc63d77c526a76b3afcf6fbb9b5e2791c19c3aa2297733208750c6e53"
+checksum = "9ba43ea6f343b788c8764558649e08df62f86c6ef251fdaeb1ffd010a9ae50a2"
[[package]]
name = "base64ct"
@@ -574,7 +576,7 @@ checksum = "c5cc78985f4d0ad1fd7b8ead06dcfaa192685775a7b1be158191c788c7d52298"
dependencies = [
"bevy_macro_utils",
"quote",
- "syn 2.0.31",
+ "syn 2.0.33",
]
[[package]]
@@ -607,7 +609,7 @@ dependencies = [
"bevy_macro_utils",
"proc-macro2",
"quote",
- "syn 2.0.31",
+ "syn 2.0.33",
]
[[package]]
@@ -634,7 +636,7 @@ checksum = "d1cd460205fe05634d58b32d9bb752b1b4eaf32b2d29cbd4161ba35eb44a2f8c"
dependencies = [
"quote",
"rustc-hash",
- "syn 2.0.31",
+ "syn 2.0.33",
"toml_edit",
]
@@ -685,7 +687,7 @@ dependencies = [
"bit-set",
"proc-macro2",
"quote",
- "syn 2.0.31",
+ "syn 2.0.33",
"uuid",
]
@@ -742,7 +744,7 @@ checksum = "0d104f29e231123c703e8b394e2341d2425c33c5a2e9ab8cc8d0a554bdb62a41"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.31",
+ "syn 2.0.33",
]
[[package]]
@@ -768,9 +770,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
-version = "2.3.3"
+version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42"
+checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635"
[[package]]
name = "block-buffer"
@@ -899,18 +901,18 @@ dependencies = [
[[package]]
name = "clap"
-version = "4.3.11"
+version = "4.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1640e5cc7fb47dbb8338fd471b105e7ed6c3cb2aeb00c2e067127ffd3764a05d"
+checksum = "84ed82781cea27b43c9b106a979fe450a13a31aab0500595fb3fc06616de08e6"
dependencies = [
"clap_builder",
]
[[package]]
name = "clap_builder"
-version = "4.3.11"
+version = "4.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "98c59138d527eeaf9b53f35a77fcc1fad9d883116070c63d5de1c7dc7b00c72b"
+checksum = "2bb9faaa7c2ef94b2743a21f5a29e6f0010dff4caa69ac8e9d6cf8b6fa74da08"
dependencies = [
"anstyle",
"clap_lex",
@@ -918,9 +920,9 @@ dependencies = [
[[package]]
name = "clap_lex"
-version = "0.5.0"
+version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b"
+checksum = "cd7cc57abe963c6d3b9d8be5b06ba7c8957a930305ca90304f24ef040aa6f961"
[[package]]
name = "compact_str"
@@ -957,9 +959,9 @@ dependencies = [
[[package]]
name = "const-oid"
-version = "0.9.4"
+version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "795bc6e66a8e340f075fcf6227e417a2dc976b92b91f3cdc778bb858778b6747"
+checksum = "28c122c3980598d243d63d9a704629a2d748d101f278052ff068be5a4423ab6f"
[[package]]
name = "convert_case"
@@ -1082,9 +1084,9 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308"
[[package]]
name = "der"
-version = "0.7.7"
+version = "0.7.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0c7ed52955ce76b1554f509074bb357d3fb8ac9b51288a65a3fd480d1dfba946"
+checksum = "fffa369a668c8af7dbf8b5e56c9f744fbd399949ed171606040001947de40b1c"
dependencies = [
"const-oid",
"pem-rfc7468",
@@ -1123,9 +1125,9 @@ checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650"
[[package]]
name = "either"
-version = "1.8.1"
+version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91"
+checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07"
[[package]]
name = "encoding_rs"
@@ -1145,7 +1147,7 @@ dependencies = [
"heck",
"proc-macro2",
"quote",
- "syn 2.0.31",
+ "syn 2.0.33",
]
[[package]]
@@ -1169,18 +1171,18 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "erased-serde"
-version = "0.3.29"
+version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fc978899517288e3ebbd1a3bfc1d9537dbb87eeab149e53ea490e63bcdff561a"
+checksum = "6c138974f9d5e7fe373eb04df7cae98833802ae4b11c24ac7039a21d5af4b26c"
dependencies = [
"serde",
]
[[package]]
name = "errno"
-version = "0.3.1"
+version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a"
+checksum = "136526188508e25c6fef639d7927dfb3e0e3084488bf202267829cf7fc23dbdd"
dependencies = [
"errno-dragonfly",
"libc",
@@ -1326,7 +1328,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.31",
+ "syn 2.0.33",
]
[[package]]
@@ -1658,9 +1660,9 @@ dependencies = [
[[package]]
name = "libc"
-version = "0.2.147"
+version = "0.2.148"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3"
+checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b"
[[package]]
name = "libm"
@@ -1676,9 +1678,9 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
[[package]]
name = "linux-raw-sys"
-version = "0.4.3"
+version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "09fc20d2ca12cb9f044c93e3bd6d32d523e6e2ec3db4f7b2939cd99026ecd3f0"
+checksum = "1a9bad9f94746442c783ca431b22403b519cd7fbeed0533fdd6328b2f2212128"
[[package]]
name = "lock_api"
@@ -1815,9 +1817,9 @@ dependencies = [
[[package]]
name = "num-complex"
-version = "0.4.3"
+version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "02e0d21255c828d6f128a1e41534206671e8c3ea0c62f32291e808dc82cff17d"
+checksum = "1ba157ca0885411de85d6ca030ba7e2a83a28636056c7c699b07c8b6f7383214"
dependencies = [
"num-traits",
]
@@ -1951,12 +1953,12 @@ checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94"
[[package]]
name = "petgraph"
-version = "0.6.3"
+version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4dd7d28ee937e54fe3080c91faa1c3a46c06de6252988a7f4592ba2310ef22a4"
+checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9"
dependencies = [
"fixedbitset",
- "indexmap 1.9.3",
+ "indexmap 2.0.0",
]
[[package]]
@@ -2038,9 +2040,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
-version = "1.0.66"
+version = "1.0.67"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9"
+checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328"
dependencies = [
"unicode-ident",
]
@@ -2279,11 +2281,11 @@ dependencies = [
[[package]]
name = "rustix"
-version = "0.38.4"
+version = "0.38.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0a962918ea88d644592894bc6dc55acc6c0956488adcebbfb6e273506b7fd6e5"
+checksum = "d7db8590df6dfcd144d22afd1b83b36c21a18d7cbc1dc4bb5295a8712e9eb662"
dependencies = [
- "bitflags 2.3.3",
+ "bitflags 2.4.0",
"errno",
"libc",
"linux-raw-sys",
@@ -2313,9 +2315,9 @@ dependencies = [
[[package]]
name = "rustls-webpki"
-version = "0.101.4"
+version = "0.101.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7d93931baf2d282fff8d3a532bbfd7653f734643161b87e3e01e59a04439bf0d"
+checksum = "45a27e3b59326c16e23d30aeb7a36a24cc0d29e71d68ff611cdfb4a01d013bed"
dependencies = [
"ring",
"untrusted",
@@ -2323,9 +2325,9 @@ dependencies = [
[[package]]
name = "rustversion"
-version = "1.0.13"
+version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dc31bd9b61a32c31f9650d18add92aa83a49ba979c143eefd27fe7177b05bd5f"
+checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4"
[[package]]
name = "ryu"
@@ -2360,9 +2362,9 @@ dependencies = [
[[package]]
name = "semver"
-version = "1.0.17"
+version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed"
+checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918"
[[package]]
name = "serde"
@@ -2375,9 +2377,9 @@ dependencies = [
[[package]]
name = "serde_bytes"
-version = "0.11.11"
+version = "0.11.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5a16be4fe5320ade08736447e3198294a5ea9a6d44dde6f35f0a5e06859c427a"
+checksum = "ab33ec92f677585af6d88c65593ae2375adde54efdbf16d597f2cbc7a6d368ff"
dependencies = [
"serde",
]
@@ -2390,14 +2392,14 @@ checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.31",
+ "syn 2.0.33",
]
[[package]]
name = "serde_json"
-version = "1.0.105"
+version = "1.0.107"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "693151e1ac27563d6dbcec9dee9fbd5da8539b20fa14ad3752b2e6d363ace360"
+checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65"
dependencies = [
"itoa",
"ryu",
@@ -2517,9 +2519,9 @@ dependencies = [
[[package]]
name = "socket2"
-version = "0.5.3"
+version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2538b18701741680e0322a2302176d3253a35388e2e62f172f64f4f16605f877"
+checksum = "4031e820eb552adee9295814c0ced9e5cf38ddf1e8b7d566d6de8e2538ea989e"
dependencies = [
"libc",
"windows-sys",
@@ -2566,9 +2568,9 @@ dependencies = [
[[package]]
name = "syn"
-version = "2.0.31"
+version = "2.0.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "718fa2415bcb8d8bd775917a1bf12a7931b6dfa890753378538118181e0cb398"
+checksum = "9caece70c63bfba29ec2fed841a09851b14a235c60010fa4de58089b6c025668"
dependencies = [
"proc-macro2",
"quote",
@@ -2601,14 +2603,14 @@ checksum = "49922ecae66cc8a249b77e68d1d0623c1b2c514f0060c27cdc68bd62a1219d35"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.31",
+ "syn 2.0.33",
]
[[package]]
name = "thread-id"
-version = "4.1.0"
+version = "4.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3ee93aa2b8331c0fec9091548843f2c90019571814057da3b783f9de09349d73"
+checksum = "79474f573561cdc4871a0de34a51c92f7f5a56039113fbb5b9c9f96bdb756669"
dependencies = [
"libc",
"redox_syscall 0.2.16",
@@ -2664,7 +2666,7 @@ dependencies = [
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
- "socket2 0.5.3",
+ "socket2 0.5.4",
"tokio-macros",
"windows-sys",
]
@@ -2677,7 +2679,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.31",
+ "syn 2.0.33",
]
[[package]]
@@ -2712,9 +2714,9 @@ checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b"
[[package]]
name = "toml_edit"
-version = "0.19.14"
+version = "0.19.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f8123f27e969974a3dfba720fdb560be359f57b44302d280ba72e76a74480e8a"
+checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421"
dependencies = [
"indexmap 2.0.0",
"toml_datetime",
@@ -2747,7 +2749,7 @@ checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.31",
+ "syn 2.0.33",
]
[[package]]
@@ -2864,9 +2866,9 @@ checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460"
[[package]]
name = "unicode-ident"
-version = "1.0.11"
+version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c"
+checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[package]]
name = "unicode-normalization"
@@ -2930,9 +2932,9 @@ checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca"
[[package]]
name = "walkdir"
-version = "2.3.3"
+version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698"
+checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee"
dependencies = [
"same-file",
"winapi-util",
@@ -2974,7 +2976,7 @@ dependencies = [
"once_cell",
"proc-macro2",
"quote",
- "syn 2.0.31",
+ "syn 2.0.33",
"wasm-bindgen-shared",
]
@@ -3008,7 +3010,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.31",
+ "syn 2.0.33",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
diff --git a/README.md b/README.md
index b83cfaa1..8d623c58 100755
--- a/README.md
+++ b/README.md
@@ -9,7 +9,7 @@ A collection of Rust crates for making Minecraft bots, clients, and tools.
-*Currently supported Minecraft version: `1.20.1`.*
+*Currently supported Minecraft version: `1.20.2`.*
## ⚠️ Azalea is still very unfinished, though most crates are in a somewhat useable state
diff --git a/azalea-client/Cargo.toml b/azalea-client/Cargo.toml
index 4c26b218..de36860e 100644
--- a/azalea-client/Cargo.toml
+++ b/azalea-client/Cargo.toml
@@ -40,6 +40,8 @@ thiserror = "^1.0.48"
tokio = { version = "^1.32.0", features = ["sync"] }
uuid = "^1.4.1"
azalea-entity = { version = "0.8.0", path = "../azalea-entity" }
+serde_json = "1.0.104"
+serde = "1.0.183"
[features]
default = ["log"]
diff --git a/azalea-client/src/chunk_batching.rs b/azalea-client/src/chunk_batching.rs
new file mode 100644
index 00000000..c0e8bb34
--- /dev/null
+++ b/azalea-client/src/chunk_batching.rs
@@ -0,0 +1,146 @@
+//! Used for Minecraft's chunk batching introduced in 23w31a (1.20.2). It's used
+//! for making the server spread out how often it sends us chunk packets
+//! depending on our receiving speed.
+
+use std::time::{Duration, Instant};
+
+use azalea_protocol::packets::game::serverbound_chunk_batch_received_packet::ServerboundChunkBatchReceivedPacket;
+use bevy_app::{App, Plugin, Update};
+use bevy_ecs::prelude::*;
+
+use crate::{
+ interact::handle_block_interact_event,
+ inventory::InventorySet,
+ local_player::{handle_send_packet_event, SendPacketEvent},
+ respawn::perform_respawn,
+};
+
+pub struct ChunkBatchingPlugin;
+impl Plugin for ChunkBatchingPlugin {
+ fn build(&self, app: &mut App) {
+ app.add_systems(
+ Update,
+ (
+ handle_chunk_batch_start_event,
+ handle_chunk_batch_finished_event,
+ )
+ .chain()
+ .before(handle_send_packet_event)
+ .before(InventorySet)
+ .before(handle_block_interact_event)
+ .before(perform_respawn),
+ )
+ .add_event::()
+ .add_event::();
+ }
+}
+
+#[derive(Component, Clone, Debug)]
+pub struct ChunkBatchInfo {
+ pub start_time: Instant,
+ pub accumulator: ChunkReceiveSpeedAccumulator,
+}
+
+#[derive(Event)]
+pub struct ChunkBatchStartEvent {
+ pub entity: Entity,
+}
+#[derive(Event)]
+pub struct ChunkBatchFinishedEvent {
+ pub entity: Entity,
+ pub batch_size: u32,
+}
+
+pub fn handle_chunk_batch_start_event(
+ mut query: Query<&mut ChunkBatchInfo>,
+ mut events: EventReader,
+) {
+ for event in events.iter() {
+ if let Ok(mut chunk_batch_info) = query.get_mut(event.entity) {
+ chunk_batch_info.start_time = Instant::now();
+ }
+ }
+}
+
+pub fn handle_chunk_batch_finished_event(
+ mut query: Query<&mut ChunkBatchInfo>,
+ mut events: EventReader,
+ mut send_packets: EventWriter,
+) {
+ for event in events.iter() {
+ if let Ok(mut chunk_batch_info) = query.get_mut(event.entity) {
+ let batch_duration = chunk_batch_info.start_time.elapsed();
+ if event.batch_size > 0 {
+ chunk_batch_info
+ .accumulator
+ .accumulate(event.batch_size, batch_duration);
+ }
+ let millis_per_chunk =
+ f64::max(0., chunk_batch_info.accumulator.get_millis_per_chunk());
+ let desired_chunks_per_tick = if millis_per_chunk == 0. {
+ // make it the server's problem instead
+ f32::NAN
+ } else {
+ (25. / millis_per_chunk) as f32
+ };
+ send_packets.send(SendPacketEvent {
+ entity: event.entity,
+ packet: ServerboundChunkBatchReceivedPacket {
+ desired_chunks_per_tick,
+ }
+ .get(),
+ });
+ }
+ }
+}
+
+#[derive(Clone, Debug)]
+pub struct ChunkReceiveSpeedAccumulator {
+ batch_sizes: Vec,
+ /// as milliseconds
+ batch_durations: Vec,
+ index: usize,
+ filled_size: usize,
+}
+impl ChunkReceiveSpeedAccumulator {
+ pub fn new(capacity: usize) -> Self {
+ Self {
+ batch_sizes: vec![0; capacity],
+ batch_durations: vec![0; capacity],
+ index: 0,
+ filled_size: 0,
+ }
+ }
+
+ pub fn accumulate(&mut self, batch_size: u32, batch_duration: Duration) {
+ self.batch_sizes[self.index] = batch_size;
+ self.batch_durations[self.index] =
+ f32::clamp(batch_duration.as_millis() as f32, 0., 15000.) as u32;
+ self.index = (self.index + 1) % self.batch_sizes.len();
+ if self.filled_size < self.batch_sizes.len() {
+ self.filled_size += 1;
+ }
+ }
+
+ pub fn get_millis_per_chunk(&self) -> f64 {
+ let mut total_batch_size = 0;
+ let mut total_batch_duration = 0;
+ for i in 0..self.filled_size {
+ total_batch_size += self.batch_sizes[i];
+ total_batch_duration += self.batch_durations[i];
+ }
+ if total_batch_size == 0 {
+ return 0.;
+ }
+ total_batch_duration as f64 / total_batch_size as f64
+ }
+}
+
+impl Default for ChunkBatchInfo {
+ fn default() -> Self {
+ Self {
+ start_time: Instant::now(),
+ accumulator: ChunkReceiveSpeedAccumulator::new(50),
+ }
+ }
+}
diff --git a/azalea-client/src/client.rs b/azalea-client/src/client.rs
index 8424bf39..cd191e0f 100644
--- a/azalea-client/src/client.rs
+++ b/azalea-client/src/client.rs
@@ -1,26 +1,29 @@
use crate::{
attack::{self, AttackPlugin},
chat::ChatPlugin,
+ chunk_batching::{ChunkBatchInfo, ChunkBatchingPlugin},
disconnect::{DisconnectEvent, DisconnectPlugin},
events::{Event, EventPlugin, LocalPlayerEvents},
interact::{CurrentSequenceNumber, InteractPlugin},
inventory::{InventoryComponent, InventoryPlugin},
local_player::{
- death_event, handle_send_packet_event, GameProfileComponent, Hunger, LocalPlayer,
- SendPacketEvent,
+ death_event, handle_send_packet_event, GameProfileComponent, Hunger, InstanceHolder,
+ PermissionLevel, PlayerAbilities, SendPacketEvent, TabList,
},
mining::{self, MinePlugin},
movement::{LastSentLookDirection, PhysicsState, PlayerMovePlugin},
- packet_handling::{self, PacketHandlerPlugin, PacketReceiver},
+ packet_handling::PacketHandlerPlugin,
player::retroactively_add_game_profile_component,
+ raw_connection::RawConnection,
respawn::RespawnPlugin,
task_pool::TaskPoolPlugin,
- Account, PlayerInfo,
+ Account, PlayerInfo, ReceivedRegistries,
};
use azalea_auth::{game_profile::GameProfile, sessionserver::ClientSessionServerError};
+use azalea_buf::McBufWritable;
use azalea_chat::FormattedText;
-use azalea_core::Vec3;
+use azalea_core::{ResourceLocation, Vec3};
use azalea_entity::{
indexing::{EntityIdIndex, Loaded},
metadata::Health,
@@ -30,19 +33,21 @@ use azalea_physics::PhysicsPlugin;
use azalea_protocol::{
connect::{Connection, ConnectionError},
packets::{
- game::{
- clientbound_player_abilities_packet::ClientboundPlayerAbilitiesPacket,
- serverbound_client_information_packet::ServerboundClientInformationPacket,
- ClientboundGamePacket, ServerboundGamePacket,
+ configuration::{
+ serverbound_client_information_packet::ClientInformation,
+ ClientboundConfigurationPacket, ServerboundConfigurationPacket,
},
- handshake::{
+ game::ServerboundGamePacket,
+ handshaking::{
client_intention_packet::ClientIntentionPacket, ClientboundHandshakePacket,
ServerboundHandshakePacket,
},
login::{
- serverbound_custom_query_packet::ServerboundCustomQueryPacket,
+ serverbound_custom_query_answer_packet::ServerboundCustomQueryAnswerPacket,
serverbound_hello_packet::ServerboundHelloPacket,
- serverbound_key_packet::ServerboundKeyPacket, ClientboundLoginPacket,
+ serverbound_key_packet::ServerboundKeyPacket,
+ serverbound_login_acknowledged_packet::ServerboundLoginAcknowledgedPacket,
+ ClientboundLoginPacket,
},
ConnectionProtocol, PROTOCOL_VERSION,
},
@@ -59,10 +64,12 @@ use bevy_ecs::{
world::World,
};
use bevy_time::{prelude::FixedTime, TimePlugin};
-use derive_more::{Deref, DerefMut};
+use derive_more::Deref;
use log::{debug, error};
use parking_lot::{Mutex, RwLock};
-use std::{collections::HashMap, fmt::Debug, io, net::SocketAddr, sync::Arc, time::Duration};
+use std::{
+ collections::HashMap, fmt::Debug, io, net::SocketAddr, ops::Deref, sync::Arc, time::Duration,
+};
use thiserror::Error;
use tokio::{
sync::{broadcast, mpsc},
@@ -71,7 +78,6 @@ use tokio::{
use uuid::Uuid;
/// `Client` has the things that a user interacting with the library will want.
-/// Things that a player in the world will want to know are in [`LocalPlayer`].
///
/// To make a new client, use either [`azalea::ClientBuilder`] or
/// [`Client::join`].
@@ -105,62 +111,6 @@ pub struct Client {
pub run_schedule_sender: mpsc::UnboundedSender<()>,
}
-/// A component that contains some of the "settings" for this client that are
-/// sent to the server, such as render distance. This is only present on local
-/// players.
-pub type ClientInformation = ServerboundClientInformationPacket;
-
-/// A component that contains the abilities the player has, like flying
-/// or instantly breaking blocks. This is only present on local players.
-#[derive(Clone, Debug, Component, Default)]
-pub struct PlayerAbilities {
- pub invulnerable: bool,
- pub flying: bool,
- pub can_fly: bool,
- /// Whether the player can instantly break blocks and can duplicate blocks
- /// in their inventory.
- pub instant_break: bool,
-
- pub flying_speed: f32,
- /// Used for the fov
- pub walking_speed: f32,
-}
-impl From for PlayerAbilities {
- fn from(packet: ClientboundPlayerAbilitiesPacket) -> Self {
- Self {
- invulnerable: packet.flags.invulnerable,
- flying: packet.flags.flying,
- can_fly: packet.flags.can_fly,
- instant_break: packet.flags.instant_break,
- flying_speed: packet.flying_speed,
- walking_speed: packet.walking_speed,
- }
- }
-}
-
-/// Level must be 0..=4
-#[derive(Component, Clone, Default, Deref, DerefMut)]
-pub struct PermissionLevel(pub u8);
-
-/// A component and resource that contains a map of player UUIDs to their
-/// information in the tab list.
-///
-/// This is a component on local players in case you want to get the tab list
-/// that a certain client is seeing, and it's also a resource in case you know
-/// that the server gives the same tab list to every player.
-///
-/// ```
-/// # use azalea_client::TabList;
-/// # fn example(client: &azalea_client::Client) {
-/// let tab_list = client.component::();
-/// println!("Online players:");
-/// for (uuid, player_info) in tab_list.iter() {
-/// println!("- {} ({}ms)", player_info.profile.name, player_info.latency);
-/// }
-/// # }
-#[derive(Component, Resource, Clone, Debug, Deref, DerefMut, Default)]
-pub struct TabList(HashMap);
-
/// An error that happened while joining the server.
#[derive(Error, Debug)]
pub enum JoinError {
@@ -261,71 +211,55 @@ impl Client {
let entity = ecs_lock.lock().spawn(account.to_owned()).id();
let conn = Connection::new(resolved_address).await?;
- let (conn, game_profile) = Self::handshake(conn, account, address).await?;
+ let (mut conn, game_profile) = Self::handshake(conn, account, address).await?;
+
+ {
+ // quickly send the brand here
+ let mut brand_data = Vec::new();
+ // they don't have to know :)
+ "vanilla".write_into(&mut brand_data).unwrap();
+ conn.write(
+ azalea_protocol::packets::configuration::serverbound_custom_payload_packet::ServerboundCustomPayloadPacket {
+ identifier: ResourceLocation::new("brand"),
+ data: brand_data.into(),
+ }
+ .get(),
+ ).await?;
+ }
+
let (read_conn, write_conn) = conn.into_split();
+ let (read_conn, write_conn) = (read_conn.raw, write_conn.raw);
// we did the handshake, so now we're connected to the server
let (tx, rx) = mpsc::unbounded_channel();
- let (packet_writer_sender, packet_writer_receiver) = mpsc::unbounded_channel();
-
- // start receiving packets
- let packet_receiver = packet_handling::PacketReceiver {
- packets: Arc::new(Mutex::new(Vec::new())),
- run_schedule_sender: run_schedule_sender.clone(),
- };
-
- let read_packets_task = tokio::spawn(packet_receiver.clone().read_task(read_conn));
- let write_packets_task = tokio::spawn(
- packet_receiver
- .clone()
- .write_task(write_conn, packet_writer_receiver),
- );
-
- let local_player = crate::local_player::LocalPlayer::new(
- entity,
- packet_writer_sender,
- // default to an empty world, it'll be set correctly later when we
- // get the login packet
- Arc::new(RwLock::new(Instance::default())),
- read_packets_task,
- write_packets_task,
- );
-
- ecs_lock
- .lock()
- .entity_mut(entity)
- .insert(JoinedClientBundle {
- local_player,
- packet_receiver,
- game_profile: GameProfileComponent(game_profile.clone()),
- physics_state: PhysicsState::default(),
- local_player_events: LocalPlayerEvents(tx),
- inventory: InventoryComponent::default(),
- client_information: ClientInformation::default(),
- tab_list: TabList::default(),
- current_sequence_number: CurrentSequenceNumber::default(),
- last_sent_direction: LastSentLookDirection::default(),
- abilities: PlayerAbilities::default(),
- permission_level: PermissionLevel::default(),
- hunger: Hunger::default(),
-
- entity_id_index: EntityIdIndex::default(),
-
- mining: mining::MineBundle::default(),
- attack: attack::AttackBundle::default(),
-
- _local: LocalEntity,
- _loaded: Loaded,
- });
+ let mut ecs = ecs_lock.lock();
+ // we got the ConfigurationConnection, so the client is now connected :)
let client = Client::new(
- game_profile,
+ game_profile.clone(),
entity,
ecs_lock.clone(),
run_schedule_sender.clone(),
);
+
+ ecs.entity_mut(entity).insert((
+ // these stay when we switch to the game state
+ LocalPlayerBundle {
+ raw_connection: RawConnection::new(
+ run_schedule_sender,
+ ConnectionProtocol::Configuration,
+ read_conn,
+ write_conn,
+ ),
+ received_registries: ReceivedRegistries::default(),
+ local_player_events: LocalPlayerEvents(tx),
+ game_profile: GameProfileComponent(game_profile),
+ },
+ InConfigurationState,
+ ));
+
Ok((client, rx))
}
@@ -340,7 +274,7 @@ impl Client {
address: &ServerAddress,
) -> Result<
(
- Connection,
+ Connection,
GameProfile,
),
JoinError,
@@ -362,7 +296,9 @@ impl Client {
conn.write(
ServerboundHelloPacket {
name: account.username.clone(),
- profile_id: account.uuid,
+ // TODO: pretty sure this should generate an offline-mode uuid instead of just
+ // Uuid::default()
+ profile_id: account.uuid.unwrap_or_default(),
}
.get(),
)
@@ -428,8 +364,13 @@ impl Client {
conn.set_compression_threshold(p.compression_threshold);
}
ClientboundLoginPacket::GameProfile(p) => {
- debug!("Got profile {:?}", p.game_profile);
- break (conn.game(), p.game_profile);
+ debug!(
+ "Got profile {:?}. handshake is finished and we're now switching to the configuration state",
+ p.game_profile
+ );
+ conn.write(ServerboundLoginAcknowledgedPacket {}.get())
+ .await?;
+ break (conn.configuration(), p.game_profile);
}
ClientboundLoginPacket::LoginDisconnect(p) => {
debug!("Got disconnect {:?}", p);
@@ -438,7 +379,7 @@ impl Client {
ClientboundLoginPacket::CustomQuery(p) => {
debug!("Got custom query {:?}", p);
conn.write(
- ServerboundCustomQueryPacket {
+ ServerboundCustomQueryAnswerPacket {
transaction_id: p.transaction_id,
data: None,
}
@@ -453,9 +394,12 @@ impl Client {
}
/// Write a packet directly to the server.
- pub fn write_packet(&self, packet: ServerboundGamePacket) {
- self.local_player_mut(&mut self.ecs.lock())
- .write_packet(packet);
+ pub fn write_packet(
+ &self,
+ packet: ServerboundGamePacket,
+ ) -> Result<(), crate::raw_connection::WritePacketError> {
+ self.raw_connection_mut(&mut self.ecs.lock())
+ .write_packet(packet)
}
/// Disconnect this client from the server by ending all tasks.
@@ -468,14 +412,24 @@ impl Client {
});
}
- pub fn local_player<'a>(&'a self, ecs: &'a mut World) -> &'a LocalPlayer {
- self.query::<&LocalPlayer>(ecs)
+ pub fn local_player<'a>(&'a self, ecs: &'a mut World) -> &'a InstanceHolder {
+ self.query::<&InstanceHolder>(ecs)
}
pub fn local_player_mut<'a>(
&'a self,
ecs: &'a mut World,
- ) -> bevy_ecs::world::Mut<'a, LocalPlayer> {
- self.query::<&mut LocalPlayer>(ecs)
+ ) -> bevy_ecs::world::Mut<'a, InstanceHolder> {
+ self.query::<&mut InstanceHolder>(ecs)
+ }
+
+ pub fn raw_connection<'a>(&'a self, ecs: &'a mut World) -> &'a RawConnection {
+ self.query::<&RawConnection>(ecs)
+ }
+ pub fn raw_connection_mut<'a>(
+ &'a self,
+ ecs: &'a mut World,
+ ) -> bevy_ecs::world::Mut<'a, RawConnection> {
+ self.query::<&mut RawConnection>(ecs)
}
/// Get a component from this client. This will clone the component and
@@ -538,8 +492,8 @@ impl Client {
/// ```
pub async fn set_client_information(
&self,
- client_information: ServerboundClientInformationPacket,
- ) -> Result<(), std::io::Error> {
+ client_information: ClientInformation,
+ ) -> Result<(), crate::raw_connection::WritePacketError> {
{
let mut ecs = self.ecs.lock();
let mut client_information_mut = self.query::<&mut ClientInformation>(&mut ecs);
@@ -551,7 +505,7 @@ impl Client {
"Sending client information (already logged in): {:?}",
client_information
);
- self.write_packet(client_information.get());
+ self.write_packet(azalea_protocol::packets::game::serverbound_client_information_packet::ServerboundClientInformationPacket { information: client_information.clone() }.get())?;
}
Ok(())
@@ -606,21 +560,33 @@ impl Client {
/// Get a map of player UUIDs to their information in the tab list.
///
- /// This is a shortcut for `bot.component::().0`.
+ /// This is a shortcut for `*bot.component::()`.
pub fn tab_list(&self) -> HashMap {
- self.component::().0
+ self.component::().deref().clone()
}
}
-/// A bundle for the components that are present on a local player that received
-/// a login packet. If you want to filter for this, just use [`LocalEntity`].
+/// The bundle of components that's shared when we're either in the
+/// `configuration` or `game` state.
+///
+/// For the components that are only present in the `game` state, see
+/// [`JoinedClientBundle`] and for the ones in the `configuration` state, see
+/// [`ConfigurationClientBundle`].
+#[derive(Bundle)]
+pub struct LocalPlayerBundle {
+ pub raw_connection: RawConnection,
+ pub received_registries: ReceivedRegistries,
+ pub local_player_events: LocalPlayerEvents,
+ pub game_profile: GameProfileComponent,
+}
+
+/// A bundle for the components that are present on a local player that is
+/// currently in the `game` protocol state. If you want to filter for this, just
+/// use [`LocalEntity`].
#[derive(Bundle)]
pub struct JoinedClientBundle {
- pub local_player: LocalPlayer,
- pub packet_receiver: PacketReceiver,
- pub game_profile: GameProfileComponent,
+ pub instance_holder: InstanceHolder,
pub physics_state: PhysicsState,
- pub local_player_events: LocalPlayerEvents,
pub inventory: InventoryComponent,
pub client_information: ClientInformation,
pub tab_list: TabList,
@@ -628,6 +594,7 @@ pub struct JoinedClientBundle {
pub last_sent_direction: LastSentLookDirection,
pub abilities: PlayerAbilities,
pub permission_level: PermissionLevel,
+ pub chunk_batch_info: ChunkBatchInfo,
pub hunger: Hunger,
pub entity_id_index: EntityIdIndex,
@@ -635,10 +602,15 @@ pub struct JoinedClientBundle {
pub mining: mining::MineBundle,
pub attack: attack::AttackBundle,
- pub _local: LocalEntity,
+ pub _local_entity: LocalEntity,
pub _loaded: Loaded,
}
+/// A marker component for local players that are currently in the
+/// `configuration` state.
+#[derive(Component)]
+pub struct InConfigurationState;
+
pub struct AzaleaPlugin;
impl Plugin for AzaleaPlugin {
fn build(&self, app: &mut App) {
@@ -790,6 +762,7 @@ impl PluginGroup for DefaultPlugins {
.add(RespawnPlugin)
.add(MinePlugin)
.add(AttackPlugin)
+ .add(ChunkBatchingPlugin)
.add(TickBroadcastPlugin);
#[cfg(feature = "log")]
{
diff --git a/azalea-client/src/disconnect.rs b/azalea-client/src/disconnect.rs
index 10aef7ba..966e5bb7 100644
--- a/azalea-client/src/disconnect.rs
+++ b/azalea-client/src/disconnect.rs
@@ -12,7 +12,7 @@ use bevy_ecs::{
};
use derive_more::Deref;
-use crate::{client::JoinedClientBundle, LocalPlayer};
+use crate::{client::JoinedClientBundle, raw_connection::RawConnection};
pub struct DisconnectPlugin;
impl Plugin for DisconnectPlugin {
@@ -21,7 +21,7 @@ impl Plugin for DisconnectPlugin {
PostUpdate,
(
update_read_packets_task_running_component,
- disconnect_on_read_packets_ended,
+ disconnect_on_connection_dead,
remove_components_from_disconnected_players,
)
.chain(),
@@ -47,25 +47,23 @@ pub fn remove_components_from_disconnected_players(
}
#[derive(Component, Clone, Copy, Debug, Deref)]
-pub struct ReadPacketsTaskRunning(bool);
+pub struct IsConnectionAlive(bool);
fn update_read_packets_task_running_component(
+ query: Query<(Entity, &RawConnection)>,
mut commands: Commands,
- local_player: Query<(Entity, &LocalPlayer)>,
) {
- for (entity, local_player) in &local_player {
- let running = !local_player.read_packets_task.is_finished();
- commands
- .entity(entity)
- .insert(ReadPacketsTaskRunning(running));
+ for (entity, raw_connection) in &query {
+ let running = raw_connection.is_alive();
+ commands.entity(entity).insert(IsConnectionAlive(running));
}
}
-fn disconnect_on_read_packets_ended(
- local_player: Query<(Entity, &ReadPacketsTaskRunning), Changed>,
+fn disconnect_on_connection_dead(
+ query: Query<(Entity, &IsConnectionAlive), Changed>,
mut disconnect_events: EventWriter,
) {
- for (entity, &read_packets_task_running) in &local_player {
- if !*read_packets_task_running {
+ for (entity, &is_connection_alive) in &query {
+ if !*is_connection_alive {
disconnect_events.send(DisconnectEvent { entity });
}
}
diff --git a/azalea-client/src/events.rs b/azalea-client/src/events.rs
index 0d34d47b..17ebd4e8 100644
--- a/azalea-client/src/events.rs
+++ b/azalea-client/src/events.rs
@@ -20,7 +20,7 @@ use tokio::sync::mpsc;
use crate::{
chat::{ChatPacket, ChatReceivedEvent},
- packet_handling::{
+ packet_handling::game::{
AddPlayerEvent, DeathEvent, KeepAliveEvent, PacketEvent, RemovePlayerEvent,
UpdatePlayerEvent,
},
@@ -115,13 +115,13 @@ impl Plugin for EventPlugin {
add_player_listener,
update_player_listener,
remove_player_listener,
- death_listener,
keepalive_listener,
+ death_listener,
),
)
.add_systems(
PreUpdate,
- init_listener.before(crate::packet_handling::process_packet_events),
+ init_listener.before(crate::packet_handling::game::process_packet_events),
)
.add_systems(FixedUpdate, tick_listener);
}
@@ -145,7 +145,7 @@ fn chat_listener(query: Query<&LocalPlayerEvents>, mut events: EventReader, mut events: EventReader, mut events: EventReader
for event in events.iter() {
let local_player_events = query
.get(event.entity)
- .expect("Non-localplayer entities shouldn't be able to receive add player events");
+ .expect("Non-local entities shouldn't be able to receive add player events");
local_player_events
.send(Event::AddPlayer(event.info.clone()))
.unwrap();
@@ -188,7 +188,7 @@ fn update_player_listener(
for event in events.iter() {
let local_player_events = query
.get(event.entity)
- .expect("Non-localplayer entities shouldn't be able to receive update player events");
+ .expect("Non-local entities shouldn't be able to receive update player events");
local_player_events
.send(Event::UpdatePlayer(event.info.clone()))
.unwrap();
@@ -202,7 +202,7 @@ fn remove_player_listener(
for event in events.iter() {
let local_player_events = query
.get(event.entity)
- .expect("Non-localplayer entities shouldn't be able to receive remove player events");
+ .expect("Non-local entities shouldn't be able to receive remove player events");
local_player_events
.send(Event::RemovePlayer(event.info.clone()))
.unwrap();
@@ -223,7 +223,7 @@ fn keepalive_listener(query: Query<&LocalPlayerEvents>, mut events: EventReader<
for event in events.iter() {
let local_player_events = query
.get(event.entity)
- .expect("Non-localplayer entities shouldn't be able to receive keepalive events");
+ .expect("Non-local entities shouldn't be able to receive keepalive events");
local_player_events
.send(Event::KeepAlive(event.id))
.unwrap();
diff --git a/azalea-client/src/interact.rs b/azalea-client/src/interact.rs
index b28baca5..dc1306e3 100644
--- a/azalea-client/src/interact.rs
+++ b/azalea-client/src/interact.rs
@@ -27,10 +27,13 @@ use derive_more::{Deref, DerefMut};
use log::warn;
use crate::{
- client::{PermissionLevel, PlayerAbilities},
+ attack::handle_attack_event,
inventory::{InventoryComponent, InventorySet},
- local_player::{handle_send_packet_event, LocalGameMode, SendPacketEvent},
- Client, LocalPlayer,
+ local_player::{
+ handle_send_packet_event, LocalGameMode, PermissionLevel, PlayerAbilities, SendPacketEvent,
+ },
+ respawn::perform_respawn,
+ Client,
};
/// A plugin that allows clients to interact with blocks in the world.
@@ -48,6 +51,9 @@ impl Plugin for InteractPlugin {
handle_swing_arm_event,
)
.before(handle_send_packet_event)
+ .after(InventorySet)
+ .after(perform_respawn)
+ .after(handle_attack_event)
.chain(),
update_modifiers_for_held_item
.after(InventorySet)
@@ -100,16 +106,12 @@ pub struct HitResultComponent(BlockHitResult);
pub fn handle_block_interact_event(
mut events: EventReader,
- mut query: Query<(
- &LocalPlayer,
- &mut CurrentSequenceNumber,
- &HitResultComponent,
- )>,
+ mut query: Query<(Entity, &mut CurrentSequenceNumber, &HitResultComponent)>,
+ mut send_packet_events: EventWriter,
) {
for event in events.iter() {
- let Ok((local_player, mut sequence_number, hit_result)) = query.get_mut(event.entity)
- else {
- warn!("Sent BlockInteractEvent for entity that isn't LocalPlayer");
+ let Ok((entity, mut sequence_number, hit_result)) = query.get_mut(event.entity) else {
+ warn!("Sent BlockInteractEvent for entity that doesn't have the required components");
continue;
};
@@ -141,14 +143,15 @@ pub fn handle_block_interact_event(
}
};
- local_player.write_packet(
- ServerboundUseItemOnPacket {
+ send_packet_events.send(SendPacketEvent {
+ entity,
+ packet: ServerboundUseItemOnPacket {
hand: InteractionHand::MainHand,
block_hit,
sequence: sequence_number.0,
}
.get(),
- )
+ })
}
}
diff --git a/azalea-client/src/inventory.rs b/azalea-client/src/inventory.rs
index da5376fc..2e8478d4 100644
--- a/azalea-client/src/inventory.rs
+++ b/azalea-client/src/inventory.rs
@@ -25,7 +25,11 @@ use bevy_ecs::{
};
use log::warn;
-use crate::{client::PlayerAbilities, local_player::handle_send_packet_event, Client, LocalPlayer};
+use crate::{
+ local_player::{handle_send_packet_event, PlayerAbilities, SendPacketEvent},
+ respawn::perform_respawn,
+ Client,
+};
pub struct InventoryPlugin;
impl Plugin for InventoryPlugin {
@@ -45,7 +49,8 @@ impl Plugin for InventoryPlugin {
handle_client_side_close_container_event,
)
.chain()
- .in_set(InventorySet),
+ .in_set(InventorySet)
+ .before(perform_respawn),
);
}
}
@@ -599,12 +604,13 @@ pub struct CloseContainerEvent {
pub id: u8,
}
fn handle_container_close_event(
+ query: Query<(Entity, &InventoryComponent)>,
mut events: EventReader,
mut client_side_events: EventWriter,
- query: Query<(&LocalPlayer, &InventoryComponent)>,
+ mut send_packet_events: EventWriter,
) {
for event in events.iter() {
- let (local_player, inventory) = query.get(event.entity).unwrap();
+ let (entity, inventory) = query.get(event.entity).unwrap();
if event.id != inventory.id {
warn!(
"Tried to close container with ID {}, but the current container ID is {}",
@@ -613,12 +619,13 @@ fn handle_container_close_event(
continue;
}
- local_player.write_packet(
- ServerboundContainerClosePacket {
+ send_packet_events.send(SendPacketEvent {
+ entity,
+ packet: ServerboundContainerClosePacket {
container_id: inventory.id,
}
.get(),
- );
+ });
client_side_events.send(ClientSideCloseContainerEvent {
entity: event.entity,
});
@@ -650,11 +657,12 @@ pub struct ContainerClickEvent {
pub operation: ClickOperation,
}
pub fn handle_container_click_event(
+ mut query: Query<(Entity, &mut InventoryComponent)>,
mut events: EventReader,
- mut query: Query<(&mut InventoryComponent, &LocalPlayer)>,
+ mut send_packet_events: EventWriter,
) {
for event in events.iter() {
- let (mut inventory, local_player) = query.get_mut(event.entity).unwrap();
+ let (entity, mut inventory) = query.get_mut(event.entity).unwrap();
if inventory.id != event.window_id {
warn!(
"Tried to click container with ID {}, but the current container ID is {}",
@@ -678,8 +686,9 @@ pub fn handle_container_click_event(
}
}
- local_player.write_packet(
- ServerboundContainerClickPacket {
+ send_packet_events.send(SendPacketEvent {
+ entity,
+ packet: ServerboundContainerClickPacket {
container_id: event.window_id,
state_id: inventory.state_id,
slot_num: event.operation.slot_num().map(|n| n as i16).unwrap_or(-999),
@@ -689,7 +698,7 @@ pub fn handle_container_click_event(
carried_item: inventory.carried.clone(),
}
.get(),
- )
+ })
}
}
diff --git a/azalea-client/src/lib.rs b/azalea-client/src/lib.rs
index 40544540..25c75bcf 100644
--- a/azalea-client/src/lib.rs
+++ b/azalea-client/src/lib.rs
@@ -13,6 +13,7 @@
mod account;
pub mod attack;
pub mod chat;
+pub mod chunk_batching;
mod client;
pub mod disconnect;
mod entity_query;
@@ -26,18 +27,20 @@ pub mod movement;
pub mod packet_handling;
pub mod ping;
mod player;
+pub mod raw_connection;
pub mod received_registries;
pub mod respawn;
pub mod task_pool;
pub use account::{Account, AccountOpts};
+pub use azalea_protocol::packets::configuration::serverbound_client_information_packet::ClientInformation;
pub use client::{
- start_ecs_runner, Client, ClientInformation, DefaultPlugins, JoinError, JoinedClientBundle,
- TabList, TickBroadcast,
+ start_ecs_runner, Client, DefaultPlugins, JoinError, JoinedClientBundle, TickBroadcast,
};
pub use events::Event;
-pub use local_player::{GameProfileComponent, LocalPlayer, SendPacketEvent};
+pub use local_player::{GameProfileComponent, InstanceHolder, SendPacketEvent, TabList};
pub use movement::{
PhysicsState, SprintDirection, StartSprintEvent, StartWalkEvent, WalkDirection,
};
pub use player::PlayerInfo;
+pub use received_registries::ReceivedRegistries;
diff --git a/azalea-client/src/local_player.rs b/azalea-client/src/local_player.rs
index 1bcb0948..2989f36e 100644
--- a/azalea-client/src/local_player.rs
+++ b/azalea-client/src/local_player.rs
@@ -1,51 +1,40 @@
-use std::{io, sync::Arc};
+use std::{collections::HashMap, io, sync::Arc};
use azalea_auth::game_profile::GameProfile;
use azalea_core::GameMode;
use azalea_entity::Dead;
-use azalea_protocol::packets::game::ServerboundGamePacket;
+use azalea_protocol::packets::game::{
+ clientbound_player_abilities_packet::ClientboundPlayerAbilitiesPacket, ServerboundGamePacket,
+};
use azalea_world::{Instance, PartialInstance};
use bevy_ecs::{
- component::Component, entity::Entity, event::EventReader, prelude::Event, query::Added,
+ component::Component, entity::Entity, event::EventReader, prelude::*, query::Added,
system::Query,
};
use derive_more::{Deref, DerefMut};
+use log::error;
use parking_lot::RwLock;
use thiserror::Error;
-use tokio::{sync::mpsc, task::JoinHandle};
+use tokio::sync::mpsc;
+use uuid::Uuid;
use crate::{
events::{Event as AzaleaEvent, LocalPlayerEvents},
- ClientInformation,
+ raw_connection::RawConnection,
+ ClientInformation, PlayerInfo,
};
-/// This is a component for our local player entities that are probably in a
-/// world. If you have access to a [`Client`], you probably don't need to care
-/// about this since `Client` gives you access to everything here.
-///
-/// You can also use the [`LocalEntity`] marker component for queries if you're
-/// only checking for a local player and don't need the contents of this
-/// component.
-///
-/// [`LocalEntity`]: azalea_entity::LocalEntity
-/// [`Client`]: crate::Client
+/// A component that keeps strong references to our [`PartialInstance`] and
+/// [`Instance`] for local players.
#[derive(Component)]
-pub struct LocalPlayer {
- pub packet_writer: mpsc::UnboundedSender,
-
+pub struct InstanceHolder {
/// The partial instance is the world this client currently has loaded. It
/// has a limited render distance.
pub partial_instance: Arc>,
/// The world is the combined [`PartialInstance`]s of all clients in the
/// same world. (Only relevant if you're using a shared world, i.e. a
/// swarm)
- pub world: Arc>,
-
- /// A task that reads packets from the server. The client is disconnected
- /// when this task ends.
- pub(crate) read_packets_task: JoinHandle<()>,
- /// A task that writes packets from the server.
- pub(crate) write_packets_task: JoinHandle<()>,
+ pub instance: Arc>,
}
/// A component only present in players that contains the [`GameProfile`] (which
@@ -64,6 +53,53 @@ pub struct LocalGameMode {
pub previous: Option,
}
+/// A component that contains the abilities the player has, like flying
+/// or instantly breaking blocks. This is only present on local players.
+#[derive(Clone, Debug, Component, Default)]
+pub struct PlayerAbilities {
+ pub invulnerable: bool,
+ pub flying: bool,
+ pub can_fly: bool,
+ /// Whether the player can instantly break blocks and can duplicate blocks
+ /// in their inventory.
+ pub instant_break: bool,
+
+ pub flying_speed: f32,
+ /// Used for the fov
+ pub walking_speed: f32,
+}
+impl From for PlayerAbilities {
+ fn from(packet: ClientboundPlayerAbilitiesPacket) -> Self {
+ Self {
+ invulnerable: packet.flags.invulnerable,
+ flying: packet.flags.flying,
+ can_fly: packet.flags.can_fly,
+ instant_break: packet.flags.instant_break,
+ flying_speed: packet.flying_speed,
+ walking_speed: packet.walking_speed,
+ }
+ }
+}
+
+/// Level must be 0..=4
+#[derive(Component, Clone, Default, Deref, DerefMut)]
+pub struct PermissionLevel(pub u8);
+
+/// A component that contains a map of player UUIDs to their information in the
+/// tab list.
+///
+/// ```
+/// # use azalea_client::TabList;
+/// # fn example(client: &azalea_client::Client) {
+/// let tab_list = client.component::();
+/// println!("Online players:");
+/// for (uuid, player_info) in tab_list.iter() {
+/// println!("- {} ({}ms)", player_info.profile.name, player_info.latency);
+/// }
+/// # }
+#[derive(Component, Resource, Clone, Debug, Deref, DerefMut, Default)]
+pub struct TabList(HashMap);
+
#[derive(Component, Clone)]
pub struct Hunger {
/// The main hunger bar. Goes from 0 to 20.
@@ -83,50 +119,24 @@ impl Default for Hunger {
}
}
-impl LocalPlayer {
- /// Create a new `LocalPlayer`.
- pub fn new(
- entity: Entity,
- packet_writer: mpsc::UnboundedSender,
- world: Arc>,
- read_packets_task: JoinHandle<()>,
- write_packets_task: JoinHandle<()>,
- ) -> Self {
+impl InstanceHolder {
+ /// Create a new `InstanceHolder`.
+ pub fn new(entity: Entity, world: Arc>) -> Self {
let client_information = ClientInformation::default();
- LocalPlayer {
- packet_writer,
-
- world,
+ InstanceHolder {
+ instance: world,
partial_instance: Arc::new(RwLock::new(PartialInstance::new(
azalea_world::calculate_chunk_storage_range(
client_information.view_distance.into(),
),
Some(entity),
))),
-
- read_packets_task,
- write_packets_task,
}
}
-
- /// Write a packet directly to the server.
- pub fn write_packet(&self, packet: ServerboundGamePacket) {
- self.packet_writer
- .send(packet)
- .expect("write_packet shouldn't be able to be called if the connection is closed");
- }
}
-impl Drop for LocalPlayer {
- /// Stop every active task when the `LocalPlayer` is dropped.
- fn drop(&mut self) {
- self.read_packets_task.abort();
- self.write_packets_task.abort();
- }
-}
-
-/// Send the "Death" event for [`LocalPlayer`]s that died with no reason.
+/// Send the "Death" event for [`LocalEntity`]s that died with no reason.
pub fn death_event(query: Query<&LocalPlayerEvents, Added>) {
for local_player_events in &query {
local_player_events.send(AzaleaEvent::Death(None)).unwrap();
@@ -160,11 +170,14 @@ pub struct SendPacketEvent {
pub fn handle_send_packet_event(
mut send_packet_events: EventReader,
- mut query: Query<&mut LocalPlayer>,
+ mut query: Query<&mut RawConnection>,
) {
for event in send_packet_events.iter() {
- if let Ok(local_player) = query.get_mut(event.entity) {
- local_player.write_packet(event.packet.clone());
+ if let Ok(raw_connection) = query.get_mut(event.entity) {
+ // debug!("Sending packet: {:?}", event.packet);
+ if let Err(e) = raw_connection.write_packet(event.packet.clone()) {
+ error!("Failed to send packet: {e}");
+ }
}
}
}
diff --git a/azalea-client/src/mining.rs b/azalea-client/src/mining.rs
index 336bfe24..5db357b8 100644
--- a/azalea-client/src/mining.rs
+++ b/azalea-client/src/mining.rs
@@ -12,13 +12,12 @@ use bevy_ecs::prelude::*;
use derive_more::{Deref, DerefMut};
use crate::{
- client::{PermissionLevel, PlayerAbilities},
interact::{
can_use_game_master_blocks, check_is_interaction_restricted, CurrentSequenceNumber,
HitResultComponent, SwingArmEvent,
},
inventory::{InventoryComponent, InventorySet},
- local_player::{LocalGameMode, SendPacketEvent},
+ local_player::{LocalGameMode, PermissionLevel, PlayerAbilities, SendPacketEvent},
Client,
};
diff --git a/azalea-client/src/movement.rs b/azalea-client/src/movement.rs
index 3bdcdeac..782e98ff 100644
--- a/azalea-client/src/movement.rs
+++ b/azalea-client/src/movement.rs
@@ -1,8 +1,8 @@
use crate::client::Client;
-use crate::local_player::LocalPlayer;
+use crate::local_player::SendPacketEvent;
use azalea_entity::{metadata::Sprinting, Attributes, Jumping};
use azalea_entity::{InLoadedChunk, LastSentPosition, LookDirection, Physics, Position};
-use azalea_physics::PhysicsSet;
+use azalea_physics::{ai_step, PhysicsSet};
use azalea_protocol::packets::game::serverbound_player_command_packet::ServerboundPlayerCommandPacket;
use azalea_protocol::packets::game::{
serverbound_move_player_pos_packet::ServerboundMovePlayerPosPacket,
@@ -12,7 +12,7 @@ use azalea_protocol::packets::game::{
};
use azalea_world::{MinecraftEntityId, MoveEntityError};
use bevy_app::{App, FixedUpdate, Plugin, Update};
-use bevy_ecs::prelude::Event;
+use bevy_ecs::prelude::{Event, EventWriter};
use bevy_ecs::{
component::Component, entity::Entity, event::EventReader, query::With,
schedule::IntoSystemConfigs, system::Query,
@@ -48,9 +48,11 @@ impl Plugin for PlayerMovePlugin {
.add_systems(
FixedUpdate,
(
- local_player_ai_step
+ (tick_controls, local_player_ai_step)
+ .chain()
.in_set(PhysicsSet)
- .before(azalea_physics::ai_step),
+ .before(ai_step),
+ send_sprinting_if_needed.after(azalea_entity::update_in_loaded_chunk),
send_position.after(PhysicsSet),
)
.chain(),
@@ -118,33 +120,28 @@ pub struct PhysicsState {
pub fn send_position(
mut query: Query<
(
- &MinecraftEntityId,
- &mut LocalPlayer,
- &mut PhysicsState,
+ Entity,
&Position,
+ &LookDirection,
+ &mut PhysicsState,
&mut LastSentPosition,
&mut Physics,
- &LookDirection,
&mut LastSentLookDirection,
- &Sprinting,
),
With,
>,
+ mut send_packet_events: EventWriter,
) {
for (
- id,
- mut local_player,
- mut physics_state,
+ entity,
position,
+ direction,
+ mut physics_state,
mut last_sent_position,
mut physics,
- direction,
mut last_direction,
- sprinting,
) in query.iter_mut()
{
- local_player.send_sprinting_if_needed(id, sprinting, &mut physics_state);
-
let packet = {
// TODO: the camera being able to be controlled by other entities isn't
// implemented yet if !self.is_controlled_camera() { return };
@@ -225,18 +222,16 @@ pub fn send_position(
};
if let Some(packet) = packet {
- local_player.write_packet(packet);
+ send_packet_events.send(SendPacketEvent { entity, packet });
}
}
}
-impl LocalPlayer {
- fn send_sprinting_if_needed(
- &mut self,
- id: &MinecraftEntityId,
- sprinting: &Sprinting,
- physics_state: &mut PhysicsState,
- ) {
+fn send_sprinting_if_needed(
+ mut query: Query<(Entity, &MinecraftEntityId, &Sprinting, &mut PhysicsState)>,
+ mut send_packet_events: EventWriter,
+) {
+ for (entity, minecraft_entity_id, sprinting, mut physics_state) in query.iter_mut() {
let was_sprinting = physics_state.was_sprinting;
if **sprinting != was_sprinting {
let sprinting_action = if **sprinting {
@@ -244,21 +239,26 @@ impl LocalPlayer {
} else {
azalea_protocol::packets::game::serverbound_player_command_packet::Action::StopSprinting
};
- self.write_packet(
- ServerboundPlayerCommandPacket {
- id: **id,
+ send_packet_events.send(SendPacketEvent {
+ entity,
+ packet: ServerboundPlayerCommandPacket {
+ id: **minecraft_entity_id,
action: sprinting_action,
data: 0,
}
.get(),
- );
+ });
physics_state.was_sprinting = **sprinting;
}
}
+}
+
+/// Update the impulse from self.move_direction. The multipler is used for
+/// sneaking.
+pub(crate) fn tick_controls(mut query: Query<&mut PhysicsState>) {
+ for mut physics_state in query.iter_mut() {
+ let multiplier: Option = None;
- /// Update the impulse from self.move_direction. The multipler is used for
- /// sneaking.
- pub(crate) fn tick_controls(multiplier: Option, physics_state: &mut PhysicsState) {
let mut forward_impulse: f32 = 0.;
let mut left_impulse: f32 = 0.;
let move_direction = physics_state.move_direction;
@@ -296,18 +296,11 @@ impl LocalPlayer {
/// automatically by the client.
pub fn local_player_ai_step(
mut query: Query<
- (
- &mut PhysicsState,
- &mut Physics,
- &mut Sprinting,
- &mut Attributes,
- ),
+ (&PhysicsState, &mut Physics, &mut Sprinting, &mut Attributes),
With,
>,
) {
- for (mut physics_state, mut physics, mut sprinting, mut attributes) in query.iter_mut() {
- LocalPlayer::tick_controls(None, &mut physics_state);
-
+ for (physics_state, mut physics, mut sprinting, mut attributes) in query.iter_mut() {
// server ai step
physics.xxa = physics_state.left_impulse;
physics.zza = physics_state.forward_impulse;
@@ -325,7 +318,7 @@ pub fn local_player_ai_step(
&& (
// !self.is_in_water()
// || self.is_underwater() &&
- has_enough_impulse_to_start_sprinting(&physics_state)
+ has_enough_impulse_to_start_sprinting(physics_state)
&& has_enough_food_to_sprint
// && !self.using_item()
// && !self.has_effect(MobEffects.BLINDNESS)
diff --git a/azalea-client/src/packet_handling/configuration.rs b/azalea-client/src/packet_handling/configuration.rs
new file mode 100644
index 00000000..6930e739
--- /dev/null
+++ b/azalea-client/src/packet_handling/configuration.rs
@@ -0,0 +1,204 @@
+use std::io::Cursor;
+use std::sync::Arc;
+
+use azalea_entity::indexing::{EntityIdIndex, Loaded};
+use azalea_protocol::packets::configuration::serverbound_finish_configuration_packet::ServerboundFinishConfigurationPacket;
+use azalea_protocol::packets::configuration::serverbound_keep_alive_packet::ServerboundKeepAlivePacket;
+use azalea_protocol::packets::configuration::serverbound_pong_packet::ServerboundPongPacket;
+use azalea_protocol::packets::configuration::serverbound_resource_pack_packet::ServerboundResourcePackPacket;
+use azalea_protocol::packets::configuration::ClientboundConfigurationPacket;
+use azalea_protocol::packets::ConnectionProtocol;
+use azalea_protocol::read::deserialize_packet;
+use azalea_world::Instance;
+use bevy_ecs::prelude::*;
+use bevy_ecs::system::SystemState;
+use log::{debug, error, warn};
+use parking_lot::RwLock;
+
+use crate::client::InConfigurationState;
+use crate::disconnect::DisconnectEvent;
+use crate::local_player::Hunger;
+use crate::packet_handling::game::KeepAliveEvent;
+use crate::raw_connection::RawConnection;
+use crate::ReceivedRegistries;
+
+#[derive(Event, Debug, Clone)]
+pub struct PacketEvent {
+ /// The client entity that received the packet.
+ pub entity: Entity,
+ /// The packet that was actually received.
+ pub packet: ClientboundConfigurationPacket,
+}
+
+pub fn send_packet_events(
+ query: Query<(Entity, &RawConnection), With>,
+ mut packet_events: ResMut>,
+) {
+ // we manually clear and send the events at the beginning of each update
+ // since otherwise it'd cause issues with events in process_packet_events
+ // running twice
+ packet_events.clear();
+ for (player_entity, raw_connection) in &query {
+ let packets_lock = raw_connection.incoming_packet_queue();
+ let mut packets = packets_lock.lock();
+ if !packets.is_empty() {
+ for raw_packet in packets.iter() {
+ let packet = match deserialize_packet::(
+ &mut Cursor::new(raw_packet),
+ ) {
+ Ok(packet) => packet,
+ Err(err) => {
+ error!("failed to read packet: {:?}", err);
+ continue;
+ }
+ };
+ packet_events.send(PacketEvent {
+ entity: player_entity,
+ packet: packet.clone(),
+ });
+ }
+ // clear the packets right after we read them
+ packets.clear();
+ }
+ }
+}
+
+pub fn process_packet_events(ecs: &mut World) {
+ let mut events_owned = Vec::new();
+ let mut system_state: SystemState> = SystemState::new(ecs);
+ let mut events = system_state.get_mut(ecs);
+ for PacketEvent {
+ entity: player_entity,
+ packet,
+ } in events.iter()
+ {
+ // we do this so `ecs` isn't borrowed for the whole loop
+ events_owned.push((*player_entity, packet.clone()));
+ }
+ for (player_entity, packet) in events_owned {
+ match packet {
+ ClientboundConfigurationPacket::RegistryData(p) => {
+ let mut system_state: SystemState> =
+ SystemState::new(ecs);
+ let mut query = system_state.get_mut(ecs);
+ let mut received_registries = query.get_mut(player_entity).unwrap();
+
+ let new_received_registries = p.registry_holder.registries;
+ // override the old registries with the new ones
+ // but if a registry wasn't sent, keep the old one
+ for (registry_name, registry) in new_received_registries {
+ received_registries
+ .registries
+ .insert(registry_name, registry);
+ }
+ }
+
+ ClientboundConfigurationPacket::CustomPayload(p) => {
+ debug!("Got custom payload packet {p:?}");
+ }
+ ClientboundConfigurationPacket::Disconnect(p) => {
+ warn!("Got disconnect packet {p:?}");
+ let mut system_state: SystemState> =
+ SystemState::new(ecs);
+ let mut disconnect_events = system_state.get_mut(ecs);
+ disconnect_events.send(DisconnectEvent {
+ entity: player_entity,
+ });
+ }
+ ClientboundConfigurationPacket::FinishConfiguration(p) => {
+ debug!("got FinishConfiguration packet: {p:?}");
+
+ let mut system_state: SystemState> =
+ SystemState::new(ecs);
+ let mut query = system_state.get_mut(ecs);
+ let mut raw_connection = query.get_mut(player_entity).unwrap();
+
+ let instance_holder = crate::local_player::InstanceHolder::new(
+ player_entity,
+ // default to an empty world, it'll be set correctly later when we
+ // get the login packet
+ Arc::new(RwLock::new(Instance::default())),
+ );
+
+ raw_connection
+ .write_packet(ServerboundFinishConfigurationPacket {}.get())
+ .expect(
+ "we should be in the right state and encoding this packet shouldn't fail",
+ );
+ raw_connection.set_state(ConnectionProtocol::Game);
+
+ // these components are added now that we're going to be in the Game state
+ ecs.entity_mut(player_entity)
+ .remove::()
+ .insert(crate::JoinedClientBundle {
+ instance_holder,
+ physics_state: crate::PhysicsState::default(),
+ inventory: crate::inventory::InventoryComponent::default(),
+ client_information: crate::ClientInformation::default(),
+ tab_list: crate::local_player::TabList::default(),
+ current_sequence_number: crate::interact::CurrentSequenceNumber::default(),
+ last_sent_direction: crate::movement::LastSentLookDirection::default(),
+ abilities: crate::local_player::PlayerAbilities::default(),
+ permission_level: crate::local_player::PermissionLevel::default(),
+ hunger: Hunger::default(),
+ chunk_batch_info: crate::chunk_batching::ChunkBatchInfo::default(),
+
+ entity_id_index: EntityIdIndex::default(),
+
+ mining: crate::mining::MineBundle::default(),
+ attack: crate::attack::AttackBundle::default(),
+
+ _local_entity: azalea_entity::LocalEntity,
+ _loaded: Loaded,
+ });
+ }
+ ClientboundConfigurationPacket::KeepAlive(p) => {
+ debug!("Got keep alive packet (in configuration) {p:?} for {player_entity:?}");
+
+ let mut system_state: SystemState<(
+ Query<&RawConnection>,
+ EventWriter,
+ )> = SystemState::new(ecs);
+ let (query, mut keepalive_events) = system_state.get_mut(ecs);
+ let raw_connection = query.get(player_entity).unwrap();
+
+ keepalive_events.send(KeepAliveEvent {
+ entity: player_entity,
+ id: p.id,
+ });
+ raw_connection
+ .write_packet(ServerboundKeepAlivePacket { id: p.id }.get())
+ .unwrap();
+ }
+ ClientboundConfigurationPacket::Ping(p) => {
+ debug!("Got ping packet {p:?}");
+
+ let mut system_state: SystemState> = SystemState::new(ecs);
+ let mut query = system_state.get_mut(ecs);
+ let raw_connection = query.get_mut(player_entity).unwrap();
+
+ raw_connection
+ .write_packet(ServerboundPongPacket { id: p.id }.get())
+ .unwrap();
+ }
+ ClientboundConfigurationPacket::ResourcePack(p) => {
+ debug!("Got resource pack packet {p:?}");
+
+ let mut system_state: SystemState> = SystemState::new(ecs);
+ let mut query = system_state.get_mut(ecs);
+ let raw_connection = query.get_mut(player_entity).unwrap();
+
+ // always accept resource pack
+ raw_connection.write_packet(
+ ServerboundResourcePackPacket { action: azalea_protocol::packets::configuration::serverbound_resource_pack_packet::Action::Accepted }.get()
+ ).unwrap();
+ }
+ ClientboundConfigurationPacket::UpdateEnabledFeatures(p) => {
+ debug!("Got update enabled features packet {p:?}");
+ }
+ ClientboundConfigurationPacket::UpdateTags(_p) => {
+ debug!("Got update tags packet");
+ }
+ }
+ }
+}
diff --git a/azalea-client/src/packet_handling.rs b/azalea-client/src/packet_handling/game.rs
similarity index 77%
rename from azalea-client/src/packet_handling.rs
rename to azalea-client/src/packet_handling/game.rs
index 6ac657d7..e0a8b017 100644
--- a/azalea-client/src/packet_handling.rs
+++ b/azalea-client/src/packet_handling/game.rs
@@ -4,62 +4,49 @@ use std::{
sync::{Arc, Weak},
};
-use azalea_buf::McBufWritable;
use azalea_chat::FormattedText;
use azalea_core::{ChunkPos, GameMode, ResourceLocation, Vec3};
use azalea_entity::{
indexing::{EntityIdIndex, EntityUuidIndex},
metadata::{apply_metadata, Health, PlayerMetadataBundle},
- Dead, EntityBundle, EntityKind, EntityUpdateSet, LastSentPosition, LoadedBy, LookDirection,
+ Dead, EntityBundle, EntityKind, LastSentPosition, LoadedBy, LocalEntity, LookDirection,
Physics, PlayerBundle, Position, RelativeEntityUpdate,
};
use azalea_nbt::NbtCompound;
use azalea_protocol::{
- connect::{ReadConnection, WriteConnection},
packets::game::{
clientbound_player_combat_kill_packet::ClientboundPlayerCombatKillPacket,
serverbound_accept_teleportation_packet::ServerboundAcceptTeleportationPacket,
- serverbound_custom_payload_packet::ServerboundCustomPayloadPacket,
serverbound_keep_alive_packet::ServerboundKeepAlivePacket,
serverbound_move_player_pos_rot_packet::ServerboundMovePlayerPosRotPacket,
serverbound_pong_packet::ServerboundPongPacket, ClientboundGamePacket,
- ServerboundGamePacket,
},
- read::ReadPacketError,
+ read::deserialize_packet,
};
use azalea_world::{Instance, InstanceContainer, InstanceName, MinecraftEntityId, PartialInstance};
-use bevy_app::{App, First, Plugin, PreUpdate, Update};
-use bevy_ecs::{
- component::Component,
- entity::Entity,
- event::{EventReader, EventWriter, Events},
- prelude::Event,
- query::Changed,
- schedule::IntoSystemConfigs,
- system::{Commands, Query, Res, ResMut, SystemState},
- world::World,
-};
+use bevy_ecs::{prelude::*, system::SystemState};
use log::{debug, error, trace, warn};
-use parking_lot::{Mutex, RwLock};
-use tokio::sync::mpsc;
+use parking_lot::RwLock;
use crate::{
chat::{ChatPacket, ChatReceivedEvent},
- client::{PlayerAbilities, TabList},
+ chunk_batching,
disconnect::DisconnectEvent,
- events::death_listener,
inventory::{
ClientSideCloseContainerEvent, InventoryComponent, MenuOpenedEvent,
SetContainerContentEvent,
},
- local_player::{GameProfileComponent, Hunger, LocalGameMode, LocalPlayer},
- received_registries::ReceivedRegistries,
- ClientInformation, PlayerInfo,
+ local_player::{
+ GameProfileComponent, Hunger, InstanceHolder, LocalGameMode, PlayerAbilities,
+ SendPacketEvent, TabList,
+ },
+ raw_connection::RawConnection,
+ ClientInformation, PlayerInfo, ReceivedRegistries,
};
/// An event that's sent when we receive a packet.
/// ```
-/// # use azalea_client::packet_handling::PacketEvent;
+/// # use azalea_client::packet_handling::game::PacketEvent;
/// # use azalea_protocol::packets::game::ClientboundGamePacket;
/// # use bevy_ecs::event::EventReader;
///
@@ -85,30 +72,6 @@ pub struct PacketEvent {
pub packet: ClientboundGamePacket,
}
-pub struct PacketHandlerPlugin;
-
-impl Plugin for PacketHandlerPlugin {
- fn build(&self, app: &mut App) {
- app.add_systems(First, send_packet_events)
- .add_systems(
- PreUpdate,
- process_packet_events
- // we want to index and deindex right after
- .before(EntityUpdateSet::Deindex),
- )
- .add_systems(Update, death_event_on_0_health.before(death_listener))
- .init_resource::>()
- .add_event::()
- .add_event::()
- .add_event::()
- .add_event::()
- .add_event::()
- .add_event::()
- .add_event::()
- .add_event::();
- }
-}
-
/// A player joined the game (or more specifically, was added to the tab
/// list of a local player).
#[derive(Event, Debug, Clone)]
@@ -143,20 +106,6 @@ pub struct DeathEvent {
pub packet: Option,
}
-pub fn death_event_on_0_health(
- query: Query<(Entity, &Health), Changed>,
- mut death_events: EventWriter,
-) {
- for (entity, health) in query.iter() {
- if **health == 0. {
- death_events.send(DeathEvent {
- entity,
- packet: None,
- });
- }
- }
-}
-
/// A KeepAlive packet is sent from the server to verify that the client is
/// still connected.
#[derive(Event, Debug, Clone)]
@@ -187,25 +136,28 @@ pub struct InstanceLoadedEvent {
pub instance: Weak>,
}
-/// Something that receives packets from the server.
-#[derive(Event, Component, Clone)]
-pub struct PacketReceiver {
- pub packets: Arc>>,
- pub run_schedule_sender: mpsc::UnboundedSender<()>,
-}
-
pub fn send_packet_events(
- query: Query<(Entity, &PacketReceiver)>,
+ query: Query<(Entity, &RawConnection), With>,
mut packet_events: ResMut>,
) {
// we manually clear and send the events at the beginning of each update
// since otherwise it'd cause issues with events in process_packet_events
// running twice
packet_events.clear();
- for (player_entity, packet_receiver) in &query {
- let mut packets = packet_receiver.packets.lock();
+ for (player_entity, raw_connection) in &query {
+ let packets_lock = raw_connection.incoming_packet_queue();
+ let mut packets = packets_lock.lock();
if !packets.is_empty() {
- for packet in packets.iter() {
+ for raw_packet in packets.iter() {
+ let packet =
+ match deserialize_packet::(&mut Cursor::new(raw_packet))
+ {
+ Ok(packet) => packet,
+ Err(err) => {
+ error!("failed to read packet: {:?}", err);
+ continue;
+ }
+ };
packet_events.send(PacketEvent {
entity: player_entity,
packet: packet.clone(),
@@ -238,37 +190,60 @@ pub fn process_packet_events(ecs: &mut World) {
let mut system_state: SystemState<(
Commands,
Query<(
- &mut LocalPlayer,
- &mut EntityIdIndex,
&GameProfileComponent,
&ClientInformation,
+ &ReceivedRegistries,
+ Option<&mut InstanceName>,
+ &mut EntityIdIndex,
+ &mut InstanceHolder,
)>,
EventWriter,
ResMut,
+ EventWriter,
)> = SystemState::new(ecs);
- let (mut commands, mut query, mut instance_loaded_events, mut instance_container) =
- system_state.get_mut(ecs);
- let (mut local_player, mut entity_id_index, game_profile, client_information) =
- query.get_mut(player_entity).unwrap();
+ let (
+ mut commands,
+ mut query,
+ mut instance_loaded_events,
+ mut instance_container,
+ mut send_packet_events,
+ ) = system_state.get_mut(ecs);
+ let (
+ game_profile,
+ client_information,
+ received_registries,
+ instance_name,
+ mut entity_id_index,
+ mut instance_holder,
+ ) = query.get_mut(player_entity).unwrap();
{
- let received_registries = ReceivedRegistries(p.registry_holder.root);
+ let new_instance_name = p.common.dimension.clone();
- let dimension = &received_registries
- .dimension_type
+ if let Some(mut instance_name) = instance_name {
+ *instance_name = instance_name.clone();
+ } else {
+ commands
+ .entity(player_entity)
+ .insert(InstanceName(new_instance_name.clone()));
+ }
+
+ let Some(dimension_type) = received_registries.dimension_type() else {
+ error!("Server didn't send dimension type registry, can't log in");
+ continue;
+ };
+ let dimension = &dimension_type
.value
.iter()
- .find(|t| t.name == p.dimension_type)
+ .find(|t| t.name == p.common.dimension_type)
.unwrap_or_else(|| {
- panic!("No dimension_type with name {}", p.dimension_type)
+ panic!("No dimension_type with name {}", p.common.dimension_type)
})
.element;
- let new_instance_name = p.dimension.clone();
-
// add this world to the instance_container (or don't if it's already
// there)
- let instance = instance_container.insert(
+ let weak_instance = instance_container.insert(
new_instance_name.clone(),
dimension.height,
dimension.min_y,
@@ -276,22 +251,22 @@ pub fn process_packet_events(ecs: &mut World) {
instance_loaded_events.send(InstanceLoadedEvent {
entity: player_entity,
name: new_instance_name.clone(),
- instance: Arc::downgrade(&instance),
+ instance: Arc::downgrade(&weak_instance),
});
// set the partial_world to an empty world
// (when we add chunks or entities those will be in the
// instance_container)
- *local_player.partial_instance.write() = PartialInstance::new(
+ *instance_holder.partial_instance.write() = PartialInstance::new(
azalea_world::calculate_chunk_storage_range(
client_information.view_distance.into(),
),
// this argument makes it so other clients don't update this player entity
- // in a shared world
+ // in a shared instance
Some(player_entity),
);
- local_player.world = instance;
+ instance_holder.instance = weak_instance;
let player_bundle = PlayerBundle {
entity: EntityBundle::new(
@@ -306,11 +281,11 @@ pub fn process_packet_events(ecs: &mut World) {
commands.entity(player_entity).insert((
MinecraftEntityId(p.player_id),
LocalGameMode {
- current: p.game_type,
- previous: p.previous_game_type.into(),
+ current: p.common.game_type,
+ previous: p.common.previous_game_type.into(),
},
// this gets overwritten later by the SetHealth packet
- received_registries,
+ received_registries.clone(),
player_bundle,
));
@@ -318,41 +293,59 @@ pub fn process_packet_events(ecs: &mut World) {
entity_id_index.insert(MinecraftEntityId(p.player_id), player_entity);
}
- // brand
- let mut brand_data = Vec::new();
- // they don't have to know :)
- "vanilla".write_into(&mut brand_data).unwrap();
- local_player.write_packet(
- ServerboundCustomPayloadPacket {
- identifier: ResourceLocation::new("brand"),
- data: brand_data.into(),
- }
- .get(),
- );
-
// send the client information that we have set
- log::debug!(
+ debug!(
"Sending client information because login: {:?}",
client_information
);
- local_player.write_packet(client_information.clone().get());
+ send_packet_events.send(SendPacketEvent {
+ entity: player_entity,
+ packet: azalea_protocol::packets::game::serverbound_client_information_packet::ServerboundClientInformationPacket { information: client_information.clone() }.get(),
+ });
system_state.apply(ecs);
}
ClientboundGamePacket::SetChunkCacheRadius(p) => {
- debug!("Got set chunk cache radius packet {:?}", p);
+ debug!("Got set chunk cache radius packet {p:?}");
}
+
+ ClientboundGamePacket::ChunkBatchStart(_p) => {
+ // the packet is empty, just a marker to tell us when the batch starts and ends
+ debug!("Got chunk batch start");
+ let mut system_state: SystemState<
+ EventWriter,
+ > = SystemState::new(ecs);
+ let mut chunk_batch_start_events = system_state.get_mut(ecs);
+
+ chunk_batch_start_events.send(chunk_batching::ChunkBatchStartEvent {
+ entity: player_entity,
+ });
+ }
+ ClientboundGamePacket::ChunkBatchFinished(p) => {
+ debug!("Got chunk batch finished {p:?}");
+
+ let mut system_state: SystemState<
+ EventWriter,
+ > = SystemState::new(ecs);
+ let mut chunk_batch_start_events = system_state.get_mut(ecs);
+
+ chunk_batch_start_events.send(chunk_batching::ChunkBatchFinishedEvent {
+ entity: player_entity,
+ batch_size: p.batch_size,
+ });
+ }
+
ClientboundGamePacket::CustomPayload(p) => {
- debug!("Got custom payload packet {:?}", p);
+ debug!("Got custom payload packet {p:?}");
}
ClientboundGamePacket::ChangeDifficulty(p) => {
- debug!("Got difficulty packet {:?}", p);
+ debug!("Got difficulty packet {p:?}");
}
ClientboundGamePacket::Commands(_p) => {
debug!("Got declare commands packet");
}
ClientboundGamePacket::PlayerAbilities(p) => {
- debug!("Got player abilities packet {:?}", p);
+ debug!("Got player abilities packet {p:?}");
let mut system_state: SystemState> =
SystemState::new(ecs);
let mut query = system_state.get_mut(ecs);
@@ -361,53 +354,46 @@ pub fn process_packet_events(ecs: &mut World) {
*player_abilities = PlayerAbilities::from(p);
}
ClientboundGamePacket::SetCarriedItem(p) => {
- debug!("Got set carried item packet {:?}", p);
+ debug!("Got set carried item packet {p:?}");
}
ClientboundGamePacket::UpdateTags(_p) => {
debug!("Got update tags packet");
}
ClientboundGamePacket::Disconnect(p) => {
- warn!("Got disconnect packet {:?}", p);
+ warn!("Got disconnect packet {p:?}");
let mut system_state: SystemState> =
SystemState::new(ecs);
let mut disconnect_events = system_state.get_mut(ecs);
disconnect_events.send(DisconnectEvent {
entity: player_entity,
});
- // bye
- return;
}
ClientboundGamePacket::UpdateRecipes(_p) => {
debug!("Got update recipes packet");
}
ClientboundGamePacket::EntityEvent(_p) => {
- // debug!("Got entity event packet {:?}", p);
+ // debug!("Got entity event packet {p:?}");
}
ClientboundGamePacket::Recipe(_p) => {
debug!("Got recipe packet");
}
ClientboundGamePacket::PlayerPosition(p) => {
// TODO: reply with teleport confirm
- debug!("Got player position packet {:?}", p);
+ debug!("Got player position packet {p:?}");
#[allow(clippy::type_complexity)]
- let mut system_state: SystemState<
+ let mut system_state: SystemState<(
Query<(
- &mut LocalPlayer,
&mut Physics,
&mut LookDirection,
&mut Position,
&mut LastSentPosition,
)>,
- > = SystemState::new(ecs);
- let mut query = system_state.get_mut(ecs);
- let Ok((
- local_player,
- mut physics,
- mut direction,
- mut position,
- mut last_sent_position,
- )) = query.get_mut(player_entity)
+ EventWriter,
+ )> = SystemState::new(ecs);
+ let (mut query, mut send_packet_events) = system_state.get_mut(ecs);
+ let Ok((mut physics, mut direction, mut position, mut last_sent_position)) =
+ query.get_mut(player_entity)
else {
continue;
};
@@ -470,9 +456,13 @@ pub fn process_packet_events(ecs: &mut World) {
**position = new_pos;
}
- local_player.write_packet(ServerboundAcceptTeleportationPacket { id: p.id }.get());
- local_player.write_packet(
- ServerboundMovePlayerPosRotPacket {
+ send_packet_events.send(SendPacketEvent {
+ entity: player_entity,
+ packet: ServerboundAcceptTeleportationPacket { id: p.id }.get(),
+ });
+ send_packet_events.send(SendPacketEvent {
+ entity: player_entity,
+ packet: ServerboundMovePlayerPosRotPacket {
x: new_pos.x,
y: new_pos.y,
z: new_pos.z,
@@ -482,10 +472,10 @@ pub fn process_packet_events(ecs: &mut World) {
on_ground: false,
}
.get(),
- );
+ });
}
ClientboundGamePacket::PlayerInfoUpdate(p) => {
- debug!("Got player info packet {:?}", p);
+ debug!("Got player info packet {p:?}");
#[allow(clippy::type_complexity)]
let mut system_state: SystemState<(
@@ -564,9 +554,10 @@ pub fn process_packet_events(ecs: &mut World) {
}
}
ClientboundGamePacket::SetChunkCacheCenter(p) => {
- debug!("Got chunk cache center packet {:?}", p);
+ debug!("Got chunk cache center packet {p:?}");
- let mut system_state: SystemState> = SystemState::new(ecs);
+ let mut system_state: SystemState> =
+ SystemState::new(ecs);
let mut query = system_state.get_mut(ecs);
let local_player = query.get_mut(player_entity).unwrap();
let mut partial_world = local_player.partial_instance.write();
@@ -575,13 +566,14 @@ pub fn process_packet_events(ecs: &mut World) {
}
ClientboundGamePacket::ChunksBiomes(_) => {}
ClientboundGamePacket::LightUpdate(_p) => {
- // debug!("Got light update packet {:?}", p);
+ // debug!("Got light update packet {p:?}");
}
ClientboundGamePacket::LevelChunkWithLight(p) => {
debug!("Got chunk with light packet {} {}", p.x, p.z);
let pos = ChunkPos::new(p.x, p.z);
- let mut system_state: SystemState> = SystemState::new(ecs);
+ let mut system_state: SystemState> =
+ SystemState::new(ecs);
let mut query = system_state.get_mut(ecs);
let local_player = query.get_mut(player_entity).unwrap();
@@ -590,7 +582,7 @@ pub fn process_packet_events(ecs: &mut World) {
// parse it again. This is only used when we have a shared
// world, since we check that the chunk isn't currently owned
// by this client.
- let shared_chunk = local_player.world.read().chunks.get(&pos);
+ let shared_chunk = local_player.instance.read().chunks.get(&pos);
let this_client_has_chunk = local_player
.partial_instance
.read()
@@ -598,7 +590,7 @@ pub fn process_packet_events(ecs: &mut World) {
.limited_get(&pos)
.is_some();
- let mut world = local_player.world.write();
+ let mut world = local_player.instance.write();
let mut partial_world = local_player.partial_instance.write();
if !this_client_has_chunk {
@@ -641,13 +633,14 @@ pub fn process_packet_events(ecs: &mut World) {
#[allow(clippy::type_complexity)]
let mut system_state: SystemState<(
Commands,
- Query<(&mut EntityIdIndex, Option<&InstanceName>)>,
+ Query<(&mut EntityIdIndex, Option<&InstanceName>, Option<&TabList>)>,
Res,
ResMut,
)> = SystemState::new(ecs);
let (mut commands, mut query, instance_container, mut entity_uuid_index) =
system_state.get_mut(ecs);
- let (mut entity_id_index, instance_name) = query.get_mut(player_entity).unwrap();
+ let (mut entity_id_index, instance_name, tab_list) =
+ query.get_mut(player_entity).unwrap();
if let Some(instance_name) = instance_name {
let bundle = p.as_entity_bundle((**instance_name).clone());
@@ -669,6 +662,15 @@ pub fn process_packet_events(ecs: &mut World) {
entity_uuid_index.insert(p.uuid, spawned.id());
}
+ if let Some(tab_list) = tab_list {
+ // technically this makes it possible for non-player entities to have
+ // GameProfileComponents but the server would have to be doing something
+ // really weird
+ if let Some(player_info) = tab_list.get(&p.uuid) {
+ spawned.insert(GameProfileComponent(player_info.profile.clone()));
+ }
+ }
+
// the bundle doesn't include the default entity metadata so we add that
// separately
p.apply_metadata(&mut spawned);
@@ -679,16 +681,16 @@ pub fn process_packet_events(ecs: &mut World) {
system_state.apply(ecs);
}
ClientboundGamePacket::SetEntityData(p) => {
- debug!("Got set entity data packet {:?}", p);
+ debug!("Got set entity data packet {p:?}");
#[allow(clippy::type_complexity)]
let mut system_state: SystemState<(
Commands,
- Query<(&EntityIdIndex, &LocalPlayer)>,
+ Query<(&EntityIdIndex, &InstanceHolder)>,
Query<&EntityKind>,
)> = SystemState::new(ecs);
let (mut commands, mut query, entity_kind_query) = system_state.get_mut(ecs);
- let (entity_id_index, local_player) = query.get_mut(player_entity).unwrap();
+ let (entity_id_index, instance_holder) = query.get_mut(player_entity).unwrap();
let entity = entity_id_index.get(&MinecraftEntityId(p.id));
@@ -701,7 +703,7 @@ pub fn process_packet_events(ecs: &mut World) {
// we use RelativeEntityUpdate because it makes sure changes aren't made
// multiple times
commands.entity(entity).add(RelativeEntityUpdate {
- partial_world: local_player.partial_instance.clone(),
+ partial_world: instance_holder.partial_instance.clone(),
update: Box::new(move |entity| {
let entity_id = entity.id();
entity.world_scope(|world| {
@@ -722,55 +724,25 @@ pub fn process_packet_events(ecs: &mut World) {
system_state.apply(ecs);
}
ClientboundGamePacket::UpdateAttributes(_p) => {
- // debug!("Got update attributes packet {:?}", p);
+ // debug!("Got update attributes packet {p:?}");
}
ClientboundGamePacket::SetEntityMotion(_p) => {
- // debug!("Got entity velocity packet {:?}", p);
+ // debug!("Got entity velocity packet {p:?}");
}
ClientboundGamePacket::SetEntityLink(p) => {
debug!("Got set entity link packet {p:?}");
}
- ClientboundGamePacket::AddPlayer(p) => {
- debug!("Got add player packet {p:?}");
-
- #[allow(clippy::type_complexity)]
- let mut system_state: SystemState<(
- Commands,
- Query<(&mut EntityIdIndex, &TabList, Option<&InstanceName>)>,
- )> = SystemState::new(ecs);
- let (mut commands, mut query) = system_state.get_mut(ecs);
- let (mut entity_id_index, tab_list, world_name) =
- query.get_mut(player_entity).unwrap();
-
- if let Some(InstanceName(world_name)) = world_name {
- let bundle = p.as_player_bundle(world_name.clone());
- let mut spawned = commands.spawn((
- MinecraftEntityId(p.id),
- LoadedBy(HashSet::from([player_entity])),
- bundle,
- ));
- entity_id_index.insert(MinecraftEntityId(p.id), spawned.id());
-
- if let Some(player_info) = tab_list.get(&p.uuid) {
- spawned.insert(GameProfileComponent(player_info.profile.clone()));
- }
- } else {
- warn!("got add player packet but we haven't gotten a login packet yet");
- }
-
- system_state.apply(ecs);
- }
ClientboundGamePacket::InitializeBorder(p) => {
- debug!("Got initialize border packet {:?}", p);
+ debug!("Got initialize border packet {p:?}");
}
ClientboundGamePacket::SetTime(_p) => {
- // debug!("Got set time packet {:?}", p);
+ // debug!("Got set time packet {p:?}");
}
ClientboundGamePacket::SetDefaultSpawnPosition(p) => {
- debug!("Got set default spawn position packet {:?}", p);
+ debug!("Got set default spawn position packet {p:?}");
}
ClientboundGamePacket::SetHealth(p) => {
- debug!("Got set health packet {:?}", p);
+ debug!("Got set health packet {p:?}");
let mut system_state: SystemState> =
SystemState::new(ecs);
@@ -785,22 +757,22 @@ pub fn process_packet_events(ecs: &mut World) {
// the Death event.
}
ClientboundGamePacket::SetExperience(p) => {
- debug!("Got set experience packet {:?}", p);
+ debug!("Got set experience packet {p:?}");
}
ClientboundGamePacket::TeleportEntity(p) => {
let mut system_state: SystemState<(
Commands,
- Query<(&EntityIdIndex, &LocalPlayer)>,
+ Query<(&EntityIdIndex, &InstanceHolder)>,
)> = SystemState::new(ecs);
let (mut commands, mut query) = system_state.get_mut(ecs);
- let (entity_id_index, local_player) = query.get_mut(player_entity).unwrap();
+ let (entity_id_index, instance_holder) = query.get_mut(player_entity).unwrap();
let entity = entity_id_index.get(&MinecraftEntityId(p.id));
if let Some(entity) = entity {
let new_pos = p.position;
commands.entity(entity).add(RelativeEntityUpdate {
- partial_world: local_player.partial_instance.clone(),
+ partial_world: instance_holder.partial_instance.clone(),
update: Box::new(move |entity| {
let mut position = entity.get_mut::().unwrap();
if new_pos != **position {
@@ -815,25 +787,25 @@ pub fn process_packet_events(ecs: &mut World) {
system_state.apply(ecs);
}
ClientboundGamePacket::UpdateAdvancements(p) => {
- debug!("Got update advancements packet {:?}", p);
+ debug!("Got update advancements packet {p:?}");
}
ClientboundGamePacket::RotateHead(_p) => {
- // debug!("Got rotate head packet {:?}", p);
+ // debug!("Got rotate head packet {p:?}");
}
ClientboundGamePacket::MoveEntityPos(p) => {
let mut system_state: SystemState<(
Commands,
- Query<(&EntityIdIndex, &LocalPlayer)>,
+ Query<(&EntityIdIndex, &InstanceHolder)>,
)> = SystemState::new(ecs);
let (mut commands, mut query) = system_state.get_mut(ecs);
- let (entity_id_index, local_player) = query.get_mut(player_entity).unwrap();
+ let (entity_id_index, instance_holder) = query.get_mut(player_entity).unwrap();
let entity = entity_id_index.get(&MinecraftEntityId(p.entity_id));
if let Some(entity) = entity {
let delta = p.delta.clone();
commands.entity(entity).add(RelativeEntityUpdate {
- partial_world: local_player.partial_instance.clone(),
+ partial_world: instance_holder.partial_instance.clone(),
update: Box::new(move |entity_mut| {
let mut position = entity_mut.get_mut::().unwrap();
let new_pos = position.with_delta(&delta);
@@ -854,17 +826,17 @@ pub fn process_packet_events(ecs: &mut World) {
ClientboundGamePacket::MoveEntityPosRot(p) => {
let mut system_state: SystemState<(
Commands,
- Query<(&EntityIdIndex, &LocalPlayer)>,
+ Query<(&EntityIdIndex, &InstanceHolder)>,
)> = SystemState::new(ecs);
let (mut commands, mut query) = system_state.get_mut(ecs);
- let (entity_id_index, local_player) = query.get_mut(player_entity).unwrap();
+ let (entity_id_index, instance_holder) = query.get_mut(player_entity).unwrap();
let entity = entity_id_index.get(&MinecraftEntityId(p.entity_id));
if let Some(entity) = entity {
let delta = p.delta.clone();
commands.entity(entity).add(RelativeEntityUpdate {
- partial_world: local_player.partial_instance.clone(),
+ partial_world: instance_holder.partial_instance.clone(),
update: Box::new(move |entity_mut| {
let mut position = entity_mut.get_mut::().unwrap();
let new_pos = position.with_delta(&delta);
@@ -884,25 +856,25 @@ pub fn process_packet_events(ecs: &mut World) {
}
ClientboundGamePacket::MoveEntityRot(_p) => {
- // debug!("Got move entity rot packet {:?}", p);
+ // debug!("Got move entity rot packet {p:?}");
}
ClientboundGamePacket::KeepAlive(p) => {
debug!("Got keep alive packet {p:?} for {player_entity:?}");
let mut system_state: SystemState<(
- Query<&mut LocalPlayer>,
EventWriter,
+ EventWriter,
)> = SystemState::new(ecs);
- let (mut query, mut keepalive_events) = system_state.get_mut(ecs);
+ let (mut keepalive_events, mut send_packet_events) = system_state.get_mut(ecs);
keepalive_events.send(KeepAliveEvent {
entity: player_entity,
id: p.id,
});
-
- let local_player = query.get_mut(player_entity).unwrap();
- local_player.write_packet(ServerboundKeepAlivePacket { id: p.id }.get());
- debug!("Sent keep alive packet {p:?} for {player_entity:?}");
+ send_packet_events.send(SendPacketEvent {
+ entity: player_entity,
+ packet: ServerboundKeepAlivePacket { id: p.id }.get(),
+ });
}
ClientboundGamePacket::RemoveEntities(p) => {
debug!("Got remove entities packet {:?}", p);
@@ -933,7 +905,7 @@ pub fn process_packet_events(ecs: &mut World) {
}
}
ClientboundGamePacket::PlayerChat(p) => {
- debug!("Got player chat packet {:?}", p);
+ debug!("Got player chat packet {p:?}");
let mut system_state: SystemState> =
SystemState::new(ecs);
@@ -945,7 +917,7 @@ pub fn process_packet_events(ecs: &mut World) {
});
}
ClientboundGamePacket::SystemChat(p) => {
- debug!("Got system chat packet {:?}", p);
+ debug!("Got system chat packet {p:?}");
let mut system_state: SystemState> =
SystemState::new(ecs);
@@ -957,32 +929,34 @@ pub fn process_packet_events(ecs: &mut World) {
});
}
ClientboundGamePacket::Sound(_p) => {
- // debug!("Got sound packet {:?}", p);
+ // debug!("Got sound packet {p:?}");
}
ClientboundGamePacket::LevelEvent(p) => {
- debug!("Got level event packet {:?}", p);
+ debug!("Got level event packet {p:?}");
}
ClientboundGamePacket::BlockUpdate(p) => {
- debug!("Got block update packet {:?}", p);
+ debug!("Got block update packet {p:?}");
- let mut system_state: SystemState> = SystemState::new(ecs);
+ let mut system_state: SystemState> =
+ SystemState::new(ecs);
let mut query = system_state.get_mut(ecs);
let local_player = query.get_mut(player_entity).unwrap();
- let world = local_player.world.write();
+ let world = local_player.instance.write();
world.chunks.set_block_state(&p.pos, p.block_state);
}
ClientboundGamePacket::Animate(p) => {
- debug!("Got animate packet {:?}", p);
+ debug!("Got animate packet {p:?}");
}
ClientboundGamePacket::SectionBlocksUpdate(p) => {
- debug!("Got section blocks update packet {:?}", p);
- let mut system_state: SystemState> = SystemState::new(ecs);
+ debug!("Got section blocks update packet {p:?}");
+ let mut system_state: SystemState> =
+ SystemState::new(ecs);
let mut query = system_state.get_mut(ecs);
let local_player = query.get_mut(player_entity).unwrap();
- let world = local_player.world.write();
+ let world = local_player.instance.write();
for state in &p.states {
world
@@ -993,7 +967,7 @@ pub fn process_packet_events(ecs: &mut World) {
ClientboundGamePacket::GameEvent(p) => {
use azalea_protocol::packets::game::clientbound_game_event_packet::EventType;
- debug!("Got game event packet {:?}", p);
+ debug!("Got game event packet {p:?}");
#[allow(clippy::single_match)]
match p.event {
@@ -1010,16 +984,16 @@ pub fn process_packet_events(ecs: &mut World) {
}
}
ClientboundGamePacket::LevelParticles(p) => {
- debug!("Got level particles packet {:?}", p);
+ debug!("Got level particles packet {p:?}");
}
ClientboundGamePacket::ServerData(p) => {
- debug!("Got server data packet {:?}", p);
+ debug!("Got server data packet {p:?}");
}
ClientboundGamePacket::SetEquipment(p) => {
- debug!("Got set equipment packet {:?}", p);
+ debug!("Got set equipment packet {p:?}");
}
ClientboundGamePacket::UpdateMobEffect(p) => {
- debug!("Got update mob effect packet {:?}", p);
+ debug!("Got update mob effect packet {p:?}");
}
ClientboundGamePacket::AddExperienceOrb(_) => {}
ClientboundGamePacket::AwardStats(_) => {}
@@ -1027,12 +1001,12 @@ pub fn process_packet_events(ecs: &mut World) {
ClientboundGamePacket::BlockDestruction(_) => {}
ClientboundGamePacket::BlockEntityData(_) => {}
ClientboundGamePacket::BlockEvent(p) => {
- debug!("Got block event packet {:?}", p);
+ debug!("Got block event packet {p:?}");
}
ClientboundGamePacket::BossEvent(_) => {}
ClientboundGamePacket::CommandSuggestions(_) => {}
ClientboundGamePacket::ContainerSetContent(p) => {
- debug!("Got container set content packet {:?}", p);
+ debug!("Got container set content packet {p:?}");
let mut system_state: SystemState<(
Query<&mut InventoryComponent>,
@@ -1058,7 +1032,7 @@ pub fn process_packet_events(ecs: &mut World) {
}
}
ClientboundGamePacket::ContainerSetData(p) => {
- debug!("Got container set data packet {:?}", p);
+ debug!("Got container set data packet {p:?}");
// let mut system_state: SystemState> =
// SystemState::new(ecs);
@@ -1072,7 +1046,7 @@ pub fn process_packet_events(ecs: &mut World) {
// see https://wiki.vg/Protocol#Set_Container_Property
}
ClientboundGamePacket::ContainerSetSlot(p) => {
- debug!("Got container set slot packet {:?}", p);
+ debug!("Got container set slot packet {p:?}");
let mut system_state: SystemState> =
SystemState::new(ecs);
@@ -1129,7 +1103,7 @@ pub fn process_packet_events(ecs: &mut World) {
ClientboundGamePacket::MoveVehicle(_) => {}
ClientboundGamePacket::OpenBook(_) => {}
ClientboundGamePacket::OpenScreen(p) => {
- debug!("Got open screen packet {:?}", p);
+ debug!("Got open screen packet {p:?}");
let mut system_state: SystemState> =
SystemState::new(ecs);
let mut menu_opened_events = system_state.get_mut(ecs);
@@ -1142,19 +1116,25 @@ pub fn process_packet_events(ecs: &mut World) {
}
ClientboundGamePacket::OpenSignEditor(_) => {}
ClientboundGamePacket::Ping(p) => {
- trace!("Got ping packet {:?}", p);
+ debug!("Got ping packet {p:?}");
- let mut system_state: SystemState> = SystemState::new(ecs);
- let mut query = system_state.get_mut(ecs);
+ let mut system_state: SystemState> =
+ SystemState::new(ecs);
+ let mut send_packet_events = system_state.get_mut(ecs);
- let local_player = query.get_mut(player_entity).unwrap();
- local_player.write_packet(ServerboundPongPacket { id: p.id }.get());
+ send_packet_events.send(SendPacketEvent {
+ entity: player_entity,
+ packet: ServerboundPongPacket { id: p.id }.get(),
+ });
+ }
+ ClientboundGamePacket::PongResponse(p) => {
+ debug!("Got pong response packet {p:?}");
}
ClientboundGamePacket::PlaceGhostRecipe(_) => {}
ClientboundGamePacket::PlayerCombatEnd(_) => {}
ClientboundGamePacket::PlayerCombatEnter(_) => {}
ClientboundGamePacket::PlayerCombatKill(p) => {
- debug!("Got player kill packet {:?}", p);
+ debug!("Got player kill packet {p:?}");
#[allow(clippy::type_complexity)]
let mut system_state: SystemState<(
@@ -1178,7 +1158,7 @@ pub fn process_packet_events(ecs: &mut World) {
ClientboundGamePacket::PlayerLookAt(_) => {}
ClientboundGamePacket::RemoveMobEffect(_) => {}
ClientboundGamePacket::ResourcePack(p) => {
- debug!("Got resource pack packet {:?}", p);
+ debug!("Got resource pack packet {p:?}");
let mut system_state: SystemState> =
SystemState::new(ecs);
@@ -1195,13 +1175,13 @@ pub fn process_packet_events(ecs: &mut World) {
system_state.apply(ecs);
}
ClientboundGamePacket::Respawn(p) => {
- debug!("Got respawn packet {:?}", p);
+ debug!("Got respawn packet {p:?}");
#[allow(clippy::type_complexity)]
let mut system_state: SystemState<(
Commands,
Query<(
- &mut LocalPlayer,
+ &mut InstanceHolder,
&GameProfileComponent,
&ClientInformation,
&ReceivedRegistries,
@@ -1211,25 +1191,29 @@ pub fn process_packet_events(ecs: &mut World) {
)> = SystemState::new(ecs);
let (mut commands, mut query, mut instance_loaded_events, mut instance_container) =
system_state.get_mut(ecs);
- let (mut local_player, game_profile, client_information, received_registries) =
+ let (mut instance_holder, game_profile, client_information, received_registries) =
query.get_mut(player_entity).unwrap();
{
- let dimension = &received_registries
- .dimension_type
+ let new_instance_name = p.common.dimension.clone();
+
+ let Some(dimension_type) = received_registries.dimension_type() else {
+ error!("Server didn't send dimension type registry, can't log in");
+ continue;
+ };
+
+ let dimension = &dimension_type
.value
.iter()
- .find(|t| t.name == p.dimension_type)
+ .find(|t| t.name == p.common.dimension_type)
.unwrap_or_else(|| {
- panic!("No dimension_type with name {}", p.dimension_type)
+ panic!("No dimension_type with name {}", p.common.dimension_type)
})
.element;
- let new_instance_name = p.dimension.clone();
-
// add this world to the instance_container (or don't if it's already
// there)
- let instance = instance_container.insert(
+ let weak_instance = instance_container.insert(
new_instance_name.clone(),
dimension.height,
dimension.min_y,
@@ -1237,19 +1221,19 @@ pub fn process_packet_events(ecs: &mut World) {
instance_loaded_events.send(InstanceLoadedEvent {
entity: player_entity,
name: new_instance_name.clone(),
- instance: Arc::downgrade(&instance),
+ instance: Arc::downgrade(&weak_instance),
});
// set the partial_world to an empty world
// (when we add chunks or entities those will be in the
// instance_container)
- *local_player.partial_instance.write() = PartialInstance::new(
+ *instance_holder.partial_instance.write() = PartialInstance::new(
azalea_world::calculate_chunk_storage_range(
client_information.view_distance.into(),
),
Some(player_entity),
);
- local_player.world = instance;
+ instance_holder.instance = weak_instance;
// this resets a bunch of our components like physics and stuff
let player_bundle = PlayerBundle {
@@ -1264,8 +1248,8 @@ pub fn process_packet_events(ecs: &mut World) {
// update the local gamemode and metadata things
commands.entity(player_entity).insert((
LocalGameMode {
- current: p.game_type,
- previous: p.previous_game_type.into(),
+ current: p.common.game_type,
+ previous: p.common.previous_game_type.into(),
},
player_bundle,
));
@@ -1301,49 +1285,11 @@ pub fn process_packet_events(ecs: &mut World) {
ClientboundGamePacket::TagQuery(_) => {}
ClientboundGamePacket::TakeItemEntity(_) => {}
ClientboundGamePacket::DisguisedChat(_) => {}
- ClientboundGamePacket::UpdateEnabledFeatures(_) => {}
ClientboundGamePacket::Bundle(_) => {}
ClientboundGamePacket::DamageEvent(_) => {}
ClientboundGamePacket::HurtAnimation(_) => {}
+
+ ClientboundGamePacket::StartConfiguration(_) => todo!(),
}
}
}
-
-impl PacketReceiver {
- /// Loop that reads from the connection and adds the packets to the queue +
- /// runs the schedule.
- pub async fn read_task(self, mut read_conn: ReadConnection) {
- loop {
- match read_conn.read().await {
- Ok(packet) => {
- self.packets.lock().push(packet);
- // tell the client to run all the systems
- self.run_schedule_sender.send(()).unwrap();
- }
- Err(error) => {
- if !matches!(*error, ReadPacketError::ConnectionClosed) {
- error!("Error reading packet from Client: {error:?}");
- }
- break;
- }
- }
- }
- }
-
- /// Consume the [`ServerboundGamePacket`] queue and actually write the
- /// packets to the server. It's like this so writing packets doesn't need to
- /// be awaited.
- pub async fn write_task(
- self,
- mut write_conn: WriteConnection,
- mut write_receiver: mpsc::UnboundedReceiver,
- ) {
- while let Some(packet) = write_receiver.recv().await {
- if let Err(err) = write_conn.write(packet).await {
- error!("Disconnecting because we couldn't write a packet: {err}.");
- break;
- };
- }
- // receiver is automatically closed when it's dropped
- }
-}
diff --git a/azalea-client/src/packet_handling/mod.rs b/azalea-client/src/packet_handling/mod.rs
new file mode 100644
index 00000000..35bdfc04
--- /dev/null
+++ b/azalea-client/src/packet_handling/mod.rs
@@ -0,0 +1,59 @@
+use azalea_entity::{metadata::Health, EntityUpdateSet};
+use bevy_app::{App, First, Plugin, PreUpdate, Update};
+use bevy_ecs::prelude::*;
+
+use crate::{chat::ChatReceivedEvent, events::death_listener};
+
+use self::game::{
+ AddPlayerEvent, DeathEvent, InstanceLoadedEvent, KeepAliveEvent, RemovePlayerEvent,
+ ResourcePackEvent, UpdatePlayerEvent,
+};
+
+pub mod configuration;
+pub mod game;
+
+pub struct PacketHandlerPlugin;
+
+pub fn death_event_on_0_health(
+ query: Query<(Entity, &Health), Changed>,
+ mut death_events: EventWriter,
+) {
+ for (entity, health) in query.iter() {
+ if **health == 0. {
+ death_events.send(DeathEvent {
+ entity,
+ packet: None,
+ });
+ }
+ }
+}
+
+impl Plugin for PacketHandlerPlugin {
+ fn build(&self, app: &mut App) {
+ app.add_systems(
+ First,
+ (game::send_packet_events, configuration::send_packet_events),
+ )
+ .add_systems(
+ PreUpdate,
+ (
+ game::process_packet_events,
+ configuration::process_packet_events,
+ )
+ // we want to index and deindex right after
+ .before(EntityUpdateSet::Deindex),
+ )
+ .add_systems(Update, death_event_on_0_health.before(death_listener))
+ // we do this instead of add_event so we can handle the events ourselves
+ .init_resource::>()
+ .init_resource::>()
+ .add_event::()
+ .add_event::()
+ .add_event::()
+ .add_event::()
+ .add_event::()
+ .add_event::()
+ .add_event::()
+ .add_event::();
+ }
+}
diff --git a/azalea-client/src/ping.rs b/azalea-client/src/ping.rs
index 8acde7a5..9064065c 100755
--- a/azalea-client/src/ping.rs
+++ b/azalea-client/src/ping.rs
@@ -3,7 +3,7 @@
use azalea_protocol::{
connect::{Connection, ConnectionError},
packets::{
- handshake::client_intention_packet::ClientIntentionPacket,
+ handshaking::client_intention_packet::ClientIntentionPacket,
status::{
clientbound_status_response_packet::ClientboundStatusResponsePacket,
serverbound_status_request_packet::ServerboundStatusRequestPacket,
diff --git a/azalea-client/src/player.rs b/azalea-client/src/player.rs
index a94340ab..1aba172a 100755
--- a/azalea-client/src/player.rs
+++ b/azalea-client/src/player.rs
@@ -8,7 +8,7 @@ use bevy_ecs::{
};
use uuid::Uuid;
-use crate::{packet_handling::AddPlayerEvent, GameProfileComponent};
+use crate::{packet_handling::game::AddPlayerEvent, GameProfileComponent};
/// A player in the tab list.
#[derive(Debug, Clone)]
diff --git a/azalea-client/src/raw_connection.rs b/azalea-client/src/raw_connection.rs
new file mode 100644
index 00000000..0df13a60
--- /dev/null
+++ b/azalea-client/src/raw_connection.rs
@@ -0,0 +1,174 @@
+use std::fmt::Debug;
+use std::sync::Arc;
+
+use azalea_protocol::{
+ connect::{RawReadConnection, RawWriteConnection},
+ packets::{ConnectionProtocol, ProtocolPacket},
+ read::ReadPacketError,
+ write::serialize_packet,
+};
+use bevy_ecs::prelude::*;
+use log::error;
+use parking_lot::Mutex;
+use thiserror::Error;
+use tokio::sync::mpsc;
+
+/// A component for clients that can read and write packets to the server. This
+/// works with raw bytes, so you'll have to serialize/deserialize packets
+/// yourself. It will do the compression and encryption for you though.
+#[derive(Component)]
+pub struct RawConnection {
+ reader: RawConnectionReader,
+ writer: RawConnectionWriter,
+
+ /// Packets sent to this will be sent to the server.
+
+ /// A task that reads packets from the server. The client is disconnected
+ /// when this task ends.
+ read_packets_task: tokio::task::JoinHandle<()>,
+ /// A task that writes packets from the server.
+ write_packets_task: tokio::task::JoinHandle<()>,
+
+ connection_protocol: ConnectionProtocol,
+}
+
+#[derive(Clone)]
+struct RawConnectionReader {
+ pub incoming_packet_queue: Arc>>>,
+ pub run_schedule_sender: mpsc::UnboundedSender<()>,
+}
+#[derive(Clone)]
+struct RawConnectionWriter {
+ pub outgoing_packets_sender: mpsc::UnboundedSender>,
+}
+
+#[derive(Error, Debug)]
+pub enum WritePacketError {
+ #[error("Wrong protocol state: expected {expected:?}, got {got:?}")]
+ WrongState {
+ expected: ConnectionProtocol,
+ got: ConnectionProtocol,
+ },
+ #[error(transparent)]
+ Encoding(#[from] azalea_protocol::write::PacketEncodeError),
+}
+
+impl RawConnection {
+ pub fn new(
+ run_schedule_sender: mpsc::UnboundedSender<()>,
+ connection_protocol: ConnectionProtocol,
+ raw_read_connection: RawReadConnection,
+ raw_write_connection: RawWriteConnection,
+ ) -> Self {
+ let (outgoing_packets_sender, outgoing_packets_receiver) = mpsc::unbounded_channel();
+
+ let incoming_packet_queue = Arc::new(Mutex::new(Vec::new()));
+
+ let reader = RawConnectionReader {
+ incoming_packet_queue: incoming_packet_queue.clone(),
+ run_schedule_sender,
+ };
+ let writer = RawConnectionWriter {
+ outgoing_packets_sender,
+ };
+
+ let read_packets_task = tokio::spawn(reader.clone().read_task(raw_read_connection));
+ let write_packets_task = tokio::spawn(
+ writer
+ .clone()
+ .write_task(raw_write_connection, outgoing_packets_receiver),
+ );
+
+ Self {
+ reader,
+ writer,
+ read_packets_task,
+ write_packets_task,
+ connection_protocol,
+ }
+ }
+
+ pub fn write_raw_packet(&self, raw_packet: Vec) {
+ self.writer
+ .outgoing_packets_sender
+ .send(raw_packet)
+ .unwrap();
+ }
+
+ /// Write the packet with the given state to the server.
+ ///
+ /// # Errors
+ ///
+ /// Returns an error if the packet is not valid for the current state, or if
+ /// encoding it failed somehow (like it's too big or something).
+ pub fn write_packet(
+ &self,
+ packet: P,
+ ) -> Result<(), WritePacketError> {
+ let raw_packet = serialize_packet(&packet)?;
+ self.write_raw_packet(raw_packet);
+ Ok(())
+ }
+
+ /// Returns whether the connection is still alive.
+ pub fn is_alive(&self) -> bool {
+ !self.read_packets_task.is_finished()
+ }
+
+ pub fn incoming_packet_queue(&self) -> Arc>>> {
+ self.reader.incoming_packet_queue.clone()
+ }
+
+ pub fn set_state(&mut self, connection_protocol: ConnectionProtocol) {
+ self.connection_protocol = connection_protocol;
+ }
+}
+
+impl RawConnectionReader {
+ /// Loop that reads from the connection and adds the packets to the queue +
+ /// runs the schedule.
+ pub async fn read_task(self, mut read_conn: RawReadConnection) {
+ loop {
+ match read_conn.read().await {
+ Ok(raw_packet) => {
+ self.incoming_packet_queue.lock().push(raw_packet);
+ // tell the client to run all the systems
+ self.run_schedule_sender.send(()).unwrap();
+ }
+ Err(error) => {
+ if !matches!(*error, ReadPacketError::ConnectionClosed) {
+ error!("Error reading packet from Client: {error:?}");
+ }
+ break;
+ }
+ }
+ }
+ }
+}
+
+impl RawConnectionWriter {
+ /// Consume the [`ServerboundGamePacket`] queue and actually write the
+ /// packets to the server. It's like this so writing packets doesn't need to
+ /// be awaited.
+ pub async fn write_task(
+ self,
+ mut write_conn: RawWriteConnection,
+ mut outgoing_packets_receiver: mpsc::UnboundedReceiver>,
+ ) {
+ while let Some(raw_packet) = outgoing_packets_receiver.recv().await {
+ if let Err(err) = write_conn.write(&raw_packet).await {
+ error!("Disconnecting because we couldn't write a packet: {err}.");
+ break;
+ };
+ }
+ // receiver is automatically closed when it's dropped
+ }
+}
+
+impl Drop for RawConnection {
+ /// Stop every active task when this `RawConnection` is dropped.
+ fn drop(&mut self) {
+ self.read_packets_task.abort();
+ self.write_packets_task.abort();
+ }
+}
diff --git a/azalea-client/src/received_registries.rs b/azalea-client/src/received_registries.rs
index 845527ae..024f5222 100644
--- a/azalea-client/src/received_registries.rs
+++ b/azalea-client/src/received_registries.rs
@@ -1,7 +1,28 @@
-use azalea_protocol::packets::game::clientbound_login_packet::registry::RegistryRoot;
-use bevy_ecs::component::Component;
-use derive_more::Deref;
+use std::collections::HashMap;
-/// The registries that the server sent us on login.
-#[derive(Clone, Debug, Component, Deref)]
-pub struct ReceivedRegistries(pub RegistryRoot);
+use azalea_core::ResourceLocation;
+use azalea_nbt::Nbt;
+use azalea_protocol::packets::configuration::clientbound_registry_data_packet::registry::{
+ DimensionTypeElement, RegistryType,
+};
+use bevy_ecs::prelude::*;
+use serde::de::DeserializeOwned;
+
+/// The registries that were sent to us during the configuration state.
+#[derive(Default, Component, Clone)]
+pub struct ReceivedRegistries {
+ pub registries: HashMap,
+}
+
+impl ReceivedRegistries {
+ fn get(&self, name: &ResourceLocation) -> Option {
+ let nbt = self.registries.get(name)?;
+ serde_json::from_value(serde_json::to_value(nbt).ok()?).ok()
+ }
+
+ /// Get the dimension type registry, or `None` if it doesn't exist. You
+ /// should do some type of error handling if this returns `None`.
+ pub fn dimension_type(&self) -> Option> {
+ self.get(&ResourceLocation::new("minecraft:dimension_type"))
+ }
+}
diff --git a/azalea-entity/src/lib.rs b/azalea-entity/src/lib.rs
index c2553102..dbec425b 100644
--- a/azalea-entity/src/lib.rs
+++ b/azalea-entity/src/lib.rs
@@ -353,6 +353,8 @@ pub struct PlayerBundle {
/// A marker component that signifies that this entity is "local" and shouldn't
/// be updated by other clients.
+///
+/// If this is for a client then all of our clients will have this.
#[derive(Component)]
pub struct LocalEntity;
diff --git a/azalea-entity/src/metadata.rs b/azalea-entity/src/metadata.rs
index 2f6a4870..8f63951f 100644
--- a/azalea-entity/src/metadata.rs
+++ b/azalea-entity/src/metadata.rs
@@ -761,9 +761,11 @@ impl Default for BlazeMetadataBundle {
}
#[derive(Component, Deref, DerefMut, Clone)]
-pub struct BlockDisplayInterpolationStartDeltaTicks(pub i32);
+pub struct BlockDisplayTransformationInterpolationStartDeltaTicks(pub i32);
#[derive(Component, Deref, DerefMut, Clone)]
-pub struct BlockDisplayInterpolationDuration(pub i32);
+pub struct BlockDisplayTransformationInterpolationDuration(pub i32);
+#[derive(Component, Deref, DerefMut, Clone)]
+pub struct BlockDisplayPosRotInterpolationDuration(pub i32);
#[derive(Component, Deref, DerefMut, Clone)]
pub struct BlockDisplayTranslation(pub Vec3);
#[derive(Component, Deref, DerefMut, Clone)]
@@ -800,50 +802,55 @@ impl BlockDisplay {
match d.index {
0..=7 => AbstractEntity::apply_metadata(entity, d)?,
8 => {
- entity.insert(BlockDisplayInterpolationStartDeltaTicks(
+ entity.insert(BlockDisplayTransformationInterpolationStartDeltaTicks(
d.value.into_int()?,
));
}
9 => {
- entity.insert(BlockDisplayInterpolationDuration(d.value.into_int()?));
+ entity.insert(BlockDisplayTransformationInterpolationDuration(
+ d.value.into_int()?,
+ ));
}
10 => {
- entity.insert(BlockDisplayTranslation(d.value.into_vector3()?));
+ entity.insert(BlockDisplayPosRotInterpolationDuration(d.value.into_int()?));
}
11 => {
- entity.insert(BlockDisplayScale(d.value.into_vector3()?));
+ entity.insert(BlockDisplayTranslation(d.value.into_vector3()?));
}
12 => {
- entity.insert(BlockDisplayLeftRotation(d.value.into_quaternion()?));
+ entity.insert(BlockDisplayScale(d.value.into_vector3()?));
}
13 => {
- entity.insert(BlockDisplayRightRotation(d.value.into_quaternion()?));
+ entity.insert(BlockDisplayLeftRotation(d.value.into_quaternion()?));
}
14 => {
- entity.insert(BlockDisplayBillboardRenderConstraints(d.value.into_byte()?));
+ entity.insert(BlockDisplayRightRotation(d.value.into_quaternion()?));
}
15 => {
- entity.insert(BlockDisplayBrightnessOverride(d.value.into_int()?));
+ entity.insert(BlockDisplayBillboardRenderConstraints(d.value.into_byte()?));
}
16 => {
- entity.insert(BlockDisplayViewRange(d.value.into_float()?));
+ entity.insert(BlockDisplayBrightnessOverride(d.value.into_int()?));
}
17 => {
- entity.insert(BlockDisplayShadowRadius(d.value.into_float()?));
+ entity.insert(BlockDisplayViewRange(d.value.into_float()?));
}
18 => {
- entity.insert(BlockDisplayShadowStrength(d.value.into_float()?));
+ entity.insert(BlockDisplayShadowRadius(d.value.into_float()?));
}
19 => {
- entity.insert(BlockDisplayWidth(d.value.into_float()?));
+ entity.insert(BlockDisplayShadowStrength(d.value.into_float()?));
}
20 => {
- entity.insert(BlockDisplayHeight(d.value.into_float()?));
+ entity.insert(BlockDisplayWidth(d.value.into_float()?));
}
21 => {
- entity.insert(BlockDisplayGlowColorOverride(d.value.into_int()?));
+ entity.insert(BlockDisplayHeight(d.value.into_float()?));
}
22 => {
+ entity.insert(BlockDisplayGlowColorOverride(d.value.into_int()?));
+ }
+ 23 => {
entity.insert(BlockState(d.value.into_block_state()?));
}
_ => {}
@@ -856,8 +863,11 @@ impl BlockDisplay {
pub struct BlockDisplayMetadataBundle {
_marker: BlockDisplay,
parent: AbstractEntityMetadataBundle,
- block_display_interpolation_start_delta_ticks: BlockDisplayInterpolationStartDeltaTicks,
- block_display_interpolation_duration: BlockDisplayInterpolationDuration,
+ block_display_transformation_interpolation_start_delta_ticks:
+ BlockDisplayTransformationInterpolationStartDeltaTicks,
+ block_display_transformation_interpolation_duration:
+ BlockDisplayTransformationInterpolationDuration,
+ block_display_pos_rot_interpolation_duration: BlockDisplayPosRotInterpolationDuration,
block_display_translation: BlockDisplayTranslation,
block_display_scale: BlockDisplayScale,
block_display_left_rotation: BlockDisplayLeftRotation,
@@ -893,10 +903,13 @@ impl Default for BlockDisplayMetadataBundle {
pose: Pose::default(),
ticks_frozen: TicksFrozen(0),
},
- block_display_interpolation_start_delta_ticks: BlockDisplayInterpolationStartDeltaTicks(
+ block_display_transformation_interpolation_start_delta_ticks:
+ BlockDisplayTransformationInterpolationStartDeltaTicks(0),
+ block_display_transformation_interpolation_duration:
+ BlockDisplayTransformationInterpolationDuration(0),
+ block_display_pos_rot_interpolation_duration: BlockDisplayPosRotInterpolationDuration(
0,
),
- block_display_interpolation_duration: BlockDisplayInterpolationDuration(0),
block_display_translation: BlockDisplayTranslation(Vec3 {
x: 0.0,
y: 0.0,
@@ -1901,7 +1914,7 @@ impl Default for DolphinMetadataBundle {
aggressive: Aggressive(false),
},
},
- treasure_pos: TreasurePos(BlockPos::new(0, 0, 0)),
+ treasure_pos: TreasurePos(Default::default()),
got_fish: GotFish(false),
moistness_level: MoistnessLevel(2400),
}
@@ -2929,7 +2942,7 @@ impl Default for FallingBlockMetadataBundle {
pose: Pose::default(),
ticks_frozen: TicksFrozen(0),
},
- start_pos: StartPos(BlockPos::new(0, 0, 0)),
+ start_pos: StartPos(Default::default()),
}
}
}
@@ -4409,9 +4422,11 @@ impl Default for ItemMetadataBundle {
}
#[derive(Component, Deref, DerefMut, Clone)]
-pub struct ItemDisplayInterpolationStartDeltaTicks(pub i32);
+pub struct ItemDisplayTransformationInterpolationStartDeltaTicks(pub i32);
#[derive(Component, Deref, DerefMut, Clone)]
-pub struct ItemDisplayInterpolationDuration(pub i32);
+pub struct ItemDisplayTransformationInterpolationDuration(pub i32);
+#[derive(Component, Deref, DerefMut, Clone)]
+pub struct ItemDisplayPosRotInterpolationDuration(pub i32);
#[derive(Component, Deref, DerefMut, Clone)]
pub struct ItemDisplayTranslation(pub Vec3);
#[derive(Component, Deref, DerefMut, Clone)]
@@ -4450,51 +4465,58 @@ impl ItemDisplay {
match d.index {
0..=7 => AbstractEntity::apply_metadata(entity, d)?,
8 => {
- entity.insert(ItemDisplayInterpolationStartDeltaTicks(d.value.into_int()?));
+ entity.insert(ItemDisplayTransformationInterpolationStartDeltaTicks(
+ d.value.into_int()?,
+ ));
}
9 => {
- entity.insert(ItemDisplayInterpolationDuration(d.value.into_int()?));
+ entity.insert(ItemDisplayTransformationInterpolationDuration(
+ d.value.into_int()?,
+ ));
}
10 => {
- entity.insert(ItemDisplayTranslation(d.value.into_vector3()?));
+ entity.insert(ItemDisplayPosRotInterpolationDuration(d.value.into_int()?));
}
11 => {
- entity.insert(ItemDisplayScale(d.value.into_vector3()?));
+ entity.insert(ItemDisplayTranslation(d.value.into_vector3()?));
}
12 => {
- entity.insert(ItemDisplayLeftRotation(d.value.into_quaternion()?));
+ entity.insert(ItemDisplayScale(d.value.into_vector3()?));
}
13 => {
- entity.insert(ItemDisplayRightRotation(d.value.into_quaternion()?));
+ entity.insert(ItemDisplayLeftRotation(d.value.into_quaternion()?));
}
14 => {
- entity.insert(ItemDisplayBillboardRenderConstraints(d.value.into_byte()?));
+ entity.insert(ItemDisplayRightRotation(d.value.into_quaternion()?));
}
15 => {
- entity.insert(ItemDisplayBrightnessOverride(d.value.into_int()?));
+ entity.insert(ItemDisplayBillboardRenderConstraints(d.value.into_byte()?));
}
16 => {
- entity.insert(ItemDisplayViewRange(d.value.into_float()?));
+ entity.insert(ItemDisplayBrightnessOverride(d.value.into_int()?));
}
17 => {
- entity.insert(ItemDisplayShadowRadius(d.value.into_float()?));
+ entity.insert(ItemDisplayViewRange(d.value.into_float()?));
}
18 => {
- entity.insert(ItemDisplayShadowStrength(d.value.into_float()?));
+ entity.insert(ItemDisplayShadowRadius(d.value.into_float()?));
}
19 => {
- entity.insert(ItemDisplayWidth(d.value.into_float()?));
+ entity.insert(ItemDisplayShadowStrength(d.value.into_float()?));
}
20 => {
- entity.insert(ItemDisplayHeight(d.value.into_float()?));
+ entity.insert(ItemDisplayWidth(d.value.into_float()?));
}
21 => {
- entity.insert(ItemDisplayGlowColorOverride(d.value.into_int()?));
+ entity.insert(ItemDisplayHeight(d.value.into_float()?));
}
22 => {
- entity.insert(ItemDisplayItemStack(d.value.into_item_stack()?));
+ entity.insert(ItemDisplayGlowColorOverride(d.value.into_int()?));
}
23 => {
+ entity.insert(ItemDisplayItemStack(d.value.into_item_stack()?));
+ }
+ 24 => {
entity.insert(ItemDisplayItemDisplay(d.value.into_byte()?));
}
_ => {}
@@ -4507,8 +4529,11 @@ impl ItemDisplay {
pub struct ItemDisplayMetadataBundle {
_marker: ItemDisplay,
parent: AbstractEntityMetadataBundle,
- item_display_interpolation_start_delta_ticks: ItemDisplayInterpolationStartDeltaTicks,
- item_display_interpolation_duration: ItemDisplayInterpolationDuration,
+ item_display_transformation_interpolation_start_delta_ticks:
+ ItemDisplayTransformationInterpolationStartDeltaTicks,
+ item_display_transformation_interpolation_duration:
+ ItemDisplayTransformationInterpolationDuration,
+ item_display_pos_rot_interpolation_duration: ItemDisplayPosRotInterpolationDuration,
item_display_translation: ItemDisplayTranslation,
item_display_scale: ItemDisplayScale,
item_display_left_rotation: ItemDisplayLeftRotation,
@@ -4545,10 +4570,11 @@ impl Default for ItemDisplayMetadataBundle {
pose: Pose::default(),
ticks_frozen: TicksFrozen(0),
},
- item_display_interpolation_start_delta_ticks: ItemDisplayInterpolationStartDeltaTicks(
- 0,
- ),
- item_display_interpolation_duration: ItemDisplayInterpolationDuration(0),
+ item_display_transformation_interpolation_start_delta_ticks:
+ ItemDisplayTransformationInterpolationStartDeltaTicks(0),
+ item_display_transformation_interpolation_duration:
+ ItemDisplayTransformationInterpolationDuration(0),
+ item_display_pos_rot_interpolation_duration: ItemDisplayPosRotInterpolationDuration(0),
item_display_translation: ItemDisplayTranslation(Vec3 {
x: 0.0,
y: 0.0,
@@ -6106,7 +6132,7 @@ impl Default for PlayerMetadataBundle {
player_absorption: PlayerAbsorption(0.0),
score: Score(0),
player_mode_customisation: PlayerModeCustomisation(0),
- player_main_hand: PlayerMainHand(1),
+ player_main_hand: PlayerMainHand(Default::default()),
shoulder_left: ShoulderLeft(azalea_nbt::Nbt::Compound(Default::default())),
shoulder_right: ShoulderRight(azalea_nbt::Nbt::Compound(Default::default())),
}
@@ -7830,9 +7856,11 @@ impl Default for TadpoleMetadataBundle {
}
#[derive(Component, Deref, DerefMut, Clone)]
-pub struct TextDisplayInterpolationStartDeltaTicks(pub i32);
+pub struct TextDisplayTransformationInterpolationStartDeltaTicks(pub i32);
#[derive(Component, Deref, DerefMut, Clone)]
-pub struct TextDisplayInterpolationDuration(pub i32);
+pub struct TextDisplayTransformationInterpolationDuration(pub i32);
+#[derive(Component, Deref, DerefMut, Clone)]
+pub struct TextDisplayPosRotInterpolationDuration(pub i32);
#[derive(Component, Deref, DerefMut, Clone)]
pub struct TextDisplayTranslation(pub Vec3);
#[derive(Component, Deref, DerefMut, Clone)]
@@ -7877,60 +7905,67 @@ impl TextDisplay {
match d.index {
0..=7 => AbstractEntity::apply_metadata(entity, d)?,
8 => {
- entity.insert(TextDisplayInterpolationStartDeltaTicks(d.value.into_int()?));
+ entity.insert(TextDisplayTransformationInterpolationStartDeltaTicks(
+ d.value.into_int()?,
+ ));
}
9 => {
- entity.insert(TextDisplayInterpolationDuration(d.value.into_int()?));
+ entity.insert(TextDisplayTransformationInterpolationDuration(
+ d.value.into_int()?,
+ ));
}
10 => {
- entity.insert(TextDisplayTranslation(d.value.into_vector3()?));
+ entity.insert(TextDisplayPosRotInterpolationDuration(d.value.into_int()?));
}
11 => {
- entity.insert(TextDisplayScale(d.value.into_vector3()?));
+ entity.insert(TextDisplayTranslation(d.value.into_vector3()?));
}
12 => {
- entity.insert(TextDisplayLeftRotation(d.value.into_quaternion()?));
+ entity.insert(TextDisplayScale(d.value.into_vector3()?));
}
13 => {
- entity.insert(TextDisplayRightRotation(d.value.into_quaternion()?));
+ entity.insert(TextDisplayLeftRotation(d.value.into_quaternion()?));
}
14 => {
- entity.insert(TextDisplayBillboardRenderConstraints(d.value.into_byte()?));
+ entity.insert(TextDisplayRightRotation(d.value.into_quaternion()?));
}
15 => {
- entity.insert(TextDisplayBrightnessOverride(d.value.into_int()?));
+ entity.insert(TextDisplayBillboardRenderConstraints(d.value.into_byte()?));
}
16 => {
- entity.insert(TextDisplayViewRange(d.value.into_float()?));
+ entity.insert(TextDisplayBrightnessOverride(d.value.into_int()?));
}
17 => {
- entity.insert(TextDisplayShadowRadius(d.value.into_float()?));
+ entity.insert(TextDisplayViewRange(d.value.into_float()?));
}
18 => {
- entity.insert(TextDisplayShadowStrength(d.value.into_float()?));
+ entity.insert(TextDisplayShadowRadius(d.value.into_float()?));
}
19 => {
- entity.insert(TextDisplayWidth(d.value.into_float()?));
+ entity.insert(TextDisplayShadowStrength(d.value.into_float()?));
}
20 => {
- entity.insert(TextDisplayHeight(d.value.into_float()?));
+ entity.insert(TextDisplayWidth(d.value.into_float()?));
}
21 => {
- entity.insert(TextDisplayGlowColorOverride(d.value.into_int()?));
+ entity.insert(TextDisplayHeight(d.value.into_float()?));
}
22 => {
- entity.insert(Text(d.value.into_formatted_text()?));
+ entity.insert(TextDisplayGlowColorOverride(d.value.into_int()?));
}
23 => {
- entity.insert(LineWidth(d.value.into_int()?));
+ entity.insert(Text(d.value.into_formatted_text()?));
}
24 => {
- entity.insert(BackgroundColor(d.value.into_int()?));
+ entity.insert(LineWidth(d.value.into_int()?));
}
25 => {
- entity.insert(TextOpacity(d.value.into_byte()?));
+ entity.insert(BackgroundColor(d.value.into_int()?));
}
26 => {
+ entity.insert(TextOpacity(d.value.into_byte()?));
+ }
+ 27 => {
entity.insert(StyleFlags(d.value.into_byte()?));
}
_ => {}
@@ -7943,8 +7978,11 @@ impl TextDisplay {
pub struct TextDisplayMetadataBundle {
_marker: TextDisplay,
parent: AbstractEntityMetadataBundle,
- text_display_interpolation_start_delta_ticks: TextDisplayInterpolationStartDeltaTicks,
- text_display_interpolation_duration: TextDisplayInterpolationDuration,
+ text_display_transformation_interpolation_start_delta_ticks:
+ TextDisplayTransformationInterpolationStartDeltaTicks,
+ text_display_transformation_interpolation_duration:
+ TextDisplayTransformationInterpolationDuration,
+ text_display_pos_rot_interpolation_duration: TextDisplayPosRotInterpolationDuration,
text_display_translation: TextDisplayTranslation,
text_display_scale: TextDisplayScale,
text_display_left_rotation: TextDisplayLeftRotation,
@@ -7984,10 +8022,11 @@ impl Default for TextDisplayMetadataBundle {
pose: Pose::default(),
ticks_frozen: TicksFrozen(0),
},
- text_display_interpolation_start_delta_ticks: TextDisplayInterpolationStartDeltaTicks(
- 0,
- ),
- text_display_interpolation_duration: TextDisplayInterpolationDuration(0),
+ text_display_transformation_interpolation_start_delta_ticks:
+ TextDisplayTransformationInterpolationStartDeltaTicks(0),
+ text_display_transformation_interpolation_duration:
+ TextDisplayTransformationInterpolationDuration(0),
+ text_display_pos_rot_interpolation_duration: TextDisplayPosRotInterpolationDuration(0),
text_display_translation: TextDisplayTranslation(Vec3 {
x: 0.0,
y: 0.0,
@@ -8486,10 +8525,10 @@ impl Default for TurtleMetadataBundle {
abstract_ageable_baby: AbstractAgeableBaby(false),
},
},
- home_pos: HomePos(BlockPos::new(0, 0, 0)),
+ home_pos: HomePos(Default::default()),
has_egg: HasEgg(false),
laying_egg: LayingEgg(false),
- travel_pos: TravelPos(BlockPos::new(0, 0, 0)),
+ travel_pos: TravelPos(Default::default()),
going_home: GoingHome(false),
travelling: Travelling(false),
}
diff --git a/azalea-entity/src/plugin/relative_updates.rs b/azalea-entity/src/plugin/relative_updates.rs
index 45b85203..4f7e478a 100644
--- a/azalea-entity/src/plugin/relative_updates.rs
+++ b/azalea-entity/src/plugin/relative_updates.rs
@@ -51,7 +51,7 @@ pub struct RelativeEntityUpdate {
/// This is used for making sure two clients don't do the same relative update
/// on an entity.
///
-/// If an entity is local (i.e. it's a client/localplayer), this component
+/// If an entity is local (i.e. it's a client/LocalEntity), this component
/// should NOT be present in the entity.
#[derive(Component, Debug, Deref, DerefMut)]
pub struct UpdatesReceived(u32);
diff --git a/azalea-language/src/en_us.json b/azalea-language/src/en_us.json
index 47072042..ee0df16b 100755
--- a/azalea-language/src/en_us.json
+++ b/azalea-language/src/en_us.json
@@ -200,6 +200,7 @@
"advancements.nether.uneasy_alliance.title": "Uneasy Alliance",
"advancements.nether.use_lodestone.description": "Use a Compass on a Lodestone",
"advancements.nether.use_lodestone.title": "Country Lode, Take Me Home",
+ "advancements.progress": "%s/%s",
"advancements.sad_label": ":(",
"advancements.story.cure_zombie_villager.description": "Weaken and then cure a Zombie Villager",
"advancements.story.cure_zombie_villager.title": "Zombie Doctor",
@@ -393,6 +394,7 @@
"attribute.name.generic.follow_range": "Mob Follow Range",
"attribute.name.generic.knockback_resistance": "Knockback Resistance",
"attribute.name.generic.luck": "Luck",
+ "attribute.name.generic.max_absorption": "Max Absorption",
"attribute.name.generic.max_health": "Max Health",
"attribute.name.generic.movement_speed": "Speed",
"attribute.name.horse.jump_strength": "Horse Jump Strength",
@@ -2154,6 +2156,7 @@
"chat.link.warning": "Never open links from people that you don't trust!",
"chat.queue": "[+%s pending lines]",
"chat.square_brackets": "[%s]",
+ "chat.tag.error": "Server sent invalid message.",
"chat.tag.modified": "Message modified by the server. Original:",
"chat.tag.not_secure": "Unverified message. Cannot be reported.",
"chat.tag.system": "Server message. Cannot be reported.",
@@ -2169,6 +2172,7 @@
"chat.type.team.text": "%s <%s> %s",
"chat.type.text": "<%s> %s",
"chat.type.text.narrate": "%s says %s",
+ "chat.validation_error": "Chat validation error",
"clear.failed.multiple": "No items were found on %s players",
"clear.failed.single": "No items were found on player %s",
"color.minecraft.black": "Black",
@@ -2237,6 +2241,7 @@
"commands.banip.invalid": "Invalid IP address or unknown player",
"commands.banip.success": "Banned IP %s: %s",
"commands.banlist.entry": "%s was banned by %s: %s",
+ "commands.banlist.entry.unknown": "(Unknown)",
"commands.banlist.list": "There are %s ban(s):",
"commands.banlist.none": "There are no bans",
"commands.bossbar.create.failed": "A bossbar already exists with the ID '%s'",
@@ -2379,6 +2384,10 @@
"commands.forceload.removed.multiple": "Unmarked %s chunks in %s from %s to %s for force loading",
"commands.forceload.removed.single": "Unmarked chunk %s in %s for force loading",
"commands.forceload.toobig": "Too many chunks in the specified area (maximum %s, specified %s)",
+ "commands.function.error.argument_not_compound": "Invalid argument type: %s, expected Compound",
+ "commands.function.error.missing_argument": "Missing argument %2$s to function %1$s",
+ "commands.function.error.missing_arguments": "Missing arguments to function %s",
+ "commands.function.error.parse": "While instantiating macro %s: Command '%s' caused error: %s",
"commands.function.success.multiple": "Executed %s command(s) from %s functions",
"commands.function.success.multiple.result": "Executed %s functions",
"commands.function.success.single": "Executed %s command(s) from function '%s'",
@@ -2452,6 +2461,12 @@
"commands.publish.failed": "Unable to host local game",
"commands.publish.started": "Local game hosted on port %s",
"commands.publish.success": "Multiplayer game is now hosted on port %s",
+ "commands.random.error.range_too_large": "The range of the random value must be at most 2147483646",
+ "commands.random.error.range_too_small": "The range of the random value must be at least 2",
+ "commands.random.reset.all.success": "Reset %s random sequence(s)",
+ "commands.random.reset.success": "Reset random sequence %s",
+ "commands.random.roll": "%s rolled %s (from %s to %s)",
+ "commands.random.sample.success": "Randomized value: %s",
"commands.recipe.give.failed": "No new recipes were learned",
"commands.recipe.give.success.multiple": "Unlocked %s recipes for %s players",
"commands.recipe.give.success.single": "Unlocked %s recipes for %s",
@@ -2649,6 +2664,8 @@
"connect.failed": "Failed to connect to the server",
"connect.joining": "Joining world...",
"connect.negotiating": "Negotiating...",
+ "connect.reconfiging": "Reconfiguring...",
+ "connect.reconfiguring": "Reconfiguring...",
"container.barrel": "Barrel",
"container.beacon": "Beacon",
"container.blast_furnace": "Blast Furnace",
@@ -2679,7 +2696,9 @@
"container.repair.cost": "Enchantment Cost: %1$s",
"container.repair.expensive": "Too Expensive!",
"container.shulkerBox": "Shulker Box",
+ "container.shulkerBox.itemCount": "%s x%s",
"container.shulkerBox.more": "and %s more...",
+ "container.shulkerBox.unknownContents": "???????",
"container.smoker": "Smoker",
"container.spectatorCantOpen": "Unable to open. Loot not generated yet.",
"container.stonecutter": "Stonecutter",
@@ -2779,6 +2798,8 @@
"dataPack.bundle.description": "Enables experimental Bundle item",
"dataPack.bundle.name": "Bundles",
"dataPack.title": "Select Data Packs",
+ "dataPack.trade_rebalance.description": "Updated trades for Villagers",
+ "dataPack.trade_rebalance.name": "Villager Trade Rebalance",
"dataPack.update_1_20.description": "New features and content for Minecraft 1.20",
"dataPack.update_1_20.name": "Update 1.20",
"dataPack.validation.back": "Go Back",
@@ -2792,90 +2813,90 @@
"datapackFailure.safeMode.failed.title": "Failed to load world in Safe Mode.",
"datapackFailure.title": "Errors in currently selected data packs prevented the world from loading.\nYou can either try to load it with only the vanilla data pack (\"safe mode\"), or go back to the title screen and fix it manually.",
"death.attack.anvil": "%1$s was squashed by a falling anvil",
- "death.attack.anvil.player": "%1$s was squashed by a falling anvil whilst fighting %2$s",
+ "death.attack.anvil.player": "%1$s was squashed by a falling anvil while fighting %2$s",
"death.attack.arrow": "%1$s was shot by %2$s",
"death.attack.arrow.item": "%1$s was shot by %2$s using %3$s",
"death.attack.badRespawnPoint.link": "Intentional Game Design",
"death.attack.badRespawnPoint.message": "%1$s was killed by %2$s",
"death.attack.cactus": "%1$s was pricked to death",
- "death.attack.cactus.player": "%1$s walked into a cactus whilst trying to escape %2$s",
+ "death.attack.cactus.player": "%1$s walked into a cactus while trying to escape %2$s",
"death.attack.cramming": "%1$s was squished too much",
"death.attack.cramming.player": "%1$s was squashed by %2$s",
"death.attack.dragonBreath": "%1$s was roasted in dragon's breath",
"death.attack.dragonBreath.player": "%1$s was roasted in dragon's breath by %2$s",
"death.attack.drown": "%1$s drowned",
- "death.attack.drown.player": "%1$s drowned whilst trying to escape %2$s",
+ "death.attack.drown.player": "%1$s drowned while trying to escape %2$s",
"death.attack.dryout": "%1$s died from dehydration",
- "death.attack.dryout.player": "%1$s died from dehydration whilst trying to escape %2$s",
+ "death.attack.dryout.player": "%1$s died from dehydration while trying to escape %2$s",
"death.attack.even_more_magic": "%1$s was killed by even more magic",
"death.attack.explosion": "%1$s blew up",
"death.attack.explosion.player": "%1$s was blown up by %2$s",
"death.attack.explosion.player.item": "%1$s was blown up by %2$s using %3$s",
"death.attack.fall": "%1$s hit the ground too hard",
- "death.attack.fall.player": "%1$s hit the ground too hard whilst trying to escape %2$s",
+ "death.attack.fall.player": "%1$s hit the ground too hard while trying to escape %2$s",
"death.attack.fallingBlock": "%1$s was squashed by a falling block",
- "death.attack.fallingBlock.player": "%1$s was squashed by a falling block whilst fighting %2$s",
+ "death.attack.fallingBlock.player": "%1$s was squashed by a falling block while fighting %2$s",
"death.attack.fallingStalactite": "%1$s was skewered by a falling stalactite",
- "death.attack.fallingStalactite.player": "%1$s was skewered by a falling stalactite whilst fighting %2$s",
+ "death.attack.fallingStalactite.player": "%1$s was skewered by a falling stalactite while fighting %2$s",
"death.attack.fireball": "%1$s was fireballed by %2$s",
"death.attack.fireball.item": "%1$s was fireballed by %2$s using %3$s",
"death.attack.fireworks": "%1$s went off with a bang",
"death.attack.fireworks.item": "%1$s went off with a bang due to a firework fired from %3$s by %2$s",
- "death.attack.fireworks.player": "%1$s went off with a bang whilst fighting %2$s",
+ "death.attack.fireworks.player": "%1$s went off with a bang while fighting %2$s",
"death.attack.flyIntoWall": "%1$s experienced kinetic energy",
- "death.attack.flyIntoWall.player": "%1$s experienced kinetic energy whilst trying to escape %2$s",
+ "death.attack.flyIntoWall.player": "%1$s experienced kinetic energy while trying to escape %2$s",
"death.attack.freeze": "%1$s froze to death",
"death.attack.freeze.player": "%1$s was frozen to death by %2$s",
"death.attack.generic": "%1$s died",
- "death.attack.genericKill": "%1$s was killed",
- "death.attack.genericKill.player": "%1$s was killed whilst fighting %2$s",
"death.attack.generic.player": "%1$s died because of %2$s",
+ "death.attack.genericKill": "%1$s was killed",
+ "death.attack.genericKill.player": "%1$s was killed while fighting %2$s",
"death.attack.hotFloor": "%1$s discovered the floor was lava",
"death.attack.hotFloor.player": "%1$s walked into the danger zone due to %2$s",
"death.attack.indirectMagic": "%1$s was killed by %2$s using magic",
"death.attack.indirectMagic.item": "%1$s was killed by %2$s using %3$s",
"death.attack.inFire": "%1$s went up in flames",
- "death.attack.inFire.player": "%1$s walked into fire whilst fighting %2$s",
+ "death.attack.inFire.player": "%1$s walked into fire while fighting %2$s",
"death.attack.inWall": "%1$s suffocated in a wall",
- "death.attack.inWall.player": "%1$s suffocated in a wall whilst fighting %2$s",
+ "death.attack.inWall.player": "%1$s suffocated in a wall while fighting %2$s",
"death.attack.lava": "%1$s tried to swim in lava",
"death.attack.lava.player": "%1$s tried to swim in lava to escape %2$s",
"death.attack.lightningBolt": "%1$s was struck by lightning",
- "death.attack.lightningBolt.player": "%1$s was struck by lightning whilst fighting %2$s",
+ "death.attack.lightningBolt.player": "%1$s was struck by lightning while fighting %2$s",
"death.attack.magic": "%1$s was killed by magic",
- "death.attack.magic.player": "%1$s was killed by magic whilst trying to escape %2$s",
- "death.attack.message_too_long": "Actually, the message was too long to deliver fully. Sorry! Here's stripped version: %s",
+ "death.attack.magic.player": "%1$s was killed by magic while trying to escape %2$s",
+ "death.attack.message_too_long": "Actually, the message was too long to deliver fully. Sorry! Here's a stripped version: %s",
"death.attack.mob": "%1$s was slain by %2$s",
"death.attack.mob.item": "%1$s was slain by %2$s using %3$s",
"death.attack.onFire": "%1$s burned to death",
- "death.attack.onFire.item": "%1$s was burnt to a crisp whilst fighting %2$s wielding %3$s",
- "death.attack.onFire.player": "%1$s was burnt to a crisp whilst fighting %2$s",
- "death.attack.outsideBorder": "%1$s left the confines of this world",
- "death.attack.outsideBorder.player": "%1$s left the confines of this world whilst fighting %2$s",
+ "death.attack.onFire.item": "%1$s was burned to a crisp while fighting %2$s wielding %3$s",
+ "death.attack.onFire.player": "%1$s was burned to a crisp while fighting %2$s",
"death.attack.outOfWorld": "%1$s fell out of the world",
"death.attack.outOfWorld.player": "%1$s didn't want to live in the same world as %2$s",
+ "death.attack.outsideBorder": "%1$s left the confines of this world",
+ "death.attack.outsideBorder.player": "%1$s left the confines of this world while fighting %2$s",
"death.attack.player": "%1$s was slain by %2$s",
"death.attack.player.item": "%1$s was slain by %2$s using %3$s",
"death.attack.sonic_boom": "%1$s was obliterated by a sonically-charged shriek",
- "death.attack.sonic_boom.item": "%1$s was obliterated by a sonically-charged shriek whilst trying to escape %2$s wielding %3$s",
- "death.attack.sonic_boom.player": "%1$s was obliterated by a sonically-charged shriek whilst trying to escape %2$s",
+ "death.attack.sonic_boom.item": "%1$s was obliterated by a sonically-charged shriek while trying to escape %2$s wielding %3$s",
+ "death.attack.sonic_boom.player": "%1$s was obliterated by a sonically-charged shriek while trying to escape %2$s",
"death.attack.stalagmite": "%1$s was impaled on a stalagmite",
- "death.attack.stalagmite.player": "%1$s was impaled on a stalagmite whilst fighting %2$s",
+ "death.attack.stalagmite.player": "%1$s was impaled on a stalagmite while fighting %2$s",
"death.attack.starve": "%1$s starved to death",
- "death.attack.starve.player": "%1$s starved to death whilst fighting %2$s",
+ "death.attack.starve.player": "%1$s starved to death while fighting %2$s",
"death.attack.sting": "%1$s was stung to death",
"death.attack.sting.item": "%1$s was stung to death by %2$s using %3$s",
"death.attack.sting.player": "%1$s was stung to death by %2$s",
"death.attack.sweetBerryBush": "%1$s was poked to death by a sweet berry bush",
- "death.attack.sweetBerryBush.player": "%1$s was poked to death by a sweet berry bush whilst trying to escape %2$s",
- "death.attack.thorns": "%1$s was killed trying to hurt %2$s",
- "death.attack.thorns.item": "%1$s was killed by %3$s trying to hurt %2$s",
+ "death.attack.sweetBerryBush.player": "%1$s was poked to death by a sweet berry bush while trying to escape %2$s",
+ "death.attack.thorns": "%1$s was killed while trying to hurt %2$s",
+ "death.attack.thorns.item": "%1$s was killed by %3$s while trying to hurt %2$s",
"death.attack.thrown": "%1$s was pummeled by %2$s",
"death.attack.thrown.item": "%1$s was pummeled by %2$s using %3$s",
"death.attack.trident": "%1$s was impaled by %2$s",
"death.attack.trident.item": "%1$s was impaled by %2$s with %3$s",
"death.attack.wither": "%1$s withered away",
- "death.attack.wither.player": "%1$s withered away whilst fighting %2$s",
+ "death.attack.wither.player": "%1$s withered away while fighting %2$s",
"death.attack.witherSkull": "%1$s was shot by a skull from %2$s",
"death.attack.witherSkull.item": "%1$s was shot by a skull from %2$s using %3$s",
"death.fell.accident.generic": "%1$s fell from a high place",
@@ -2893,6 +2914,7 @@
"deathScreen.quit.confirm": "Are you sure you want to quit?",
"deathScreen.respawn": "Respawn",
"deathScreen.score": "Score",
+ "deathScreen.score.value": "Score: %s",
"deathScreen.spectate": "Spectate World",
"deathScreen.title": "You Died!",
"deathScreen.title.hardcore": "Game Over!",
@@ -3242,9 +3264,13 @@
"entity.not_summonable": "Can't summon entity of type %s",
"event.minecraft.raid": "Raid",
"event.minecraft.raid.defeat": "Defeat",
+ "event.minecraft.raid.defeat.full": "Raid - Defeat",
"event.minecraft.raid.raiders_remaining": "Raiders Remaining: %s",
"event.minecraft.raid.victory": "Victory",
+ "event.minecraft.raid.victory.full": "Raid - Victory",
"filled_map.buried_treasure": "Buried Treasure Map",
+ "filled_map.explorer_jungle": "Jungle Explorer Map",
+ "filled_map.explorer_swamp": "Swamp Explorer Map",
"filled_map.id": "Id #%s",
"filled_map.level": "(Level %s/%s)",
"filled_map.locked": "Locked",
@@ -3252,6 +3278,11 @@
"filled_map.monument": "Ocean Explorer Map",
"filled_map.scale": "Scaling at 1:%s",
"filled_map.unknown": "Unknown Map",
+ "filled_map.village_desert": "Desert Village Map",
+ "filled_map.village_plains": "Plains Village Map",
+ "filled_map.village_savanna": "Savanna Village Map",
+ "filled_map.village_snowy": "Snowy Village Map",
+ "filled_map.village_taiga": "Taiga Village Map",
"flat_world_preset.minecraft.bottomless_pit": "Bottomless Pit",
"flat_world_preset.minecraft.classic_flat": "Classic Flat",
"flat_world_preset.minecraft.desert": "Desert",
@@ -3304,6 +3335,8 @@
"gamerule.doWardenSpawning": "Spawn Wardens",
"gamerule.doWeatherCycle": "Update weather",
"gamerule.drowningDamage": "Deal drowning damage",
+ "gamerule.enderPearlsVanishOnDeath": "Thrown ender pearls vanish on death",
+ "gamerule.enderPearlsVanishOnDeath.description": "Whether ender pearls thrown by a player vanish when that player dies.",
"gamerule.fallDamage": "Deal fall damage",
"gamerule.fireDamage": "Deal fire damage",
"gamerule.forgiveDeadPlayers": "Forgive dead players",
@@ -3350,41 +3383,71 @@
"generator.minecraft.single_biome_surface": "Single Biome",
"generator.single_biome_caves": "Caves",
"generator.single_biome_floating_islands": "Floating Islands",
+ "gui.abuseReport.comments": "Comments",
+ "gui.abuseReport.describe": "Sharing details will help us make a well-informed decision.",
+ "gui.abuseReport.discard.content": "If you leave, you'll lose this report and your comments.\nAre you sure you want to leave?",
+ "gui.abuseReport.discard.discard": "Leave and Discard Report",
+ "gui.abuseReport.discard.draft": "Save as Draft",
+ "gui.abuseReport.discard.return": "Continue Editing",
+ "gui.abuseReport.discard.title": "Discard report and comments?",
+ "gui.abuseReport.draft.content": "Would you like to continue editing the existing report or discard it and create a new one?",
+ "gui.abuseReport.draft.discard": "Discard",
+ "gui.abuseReport.draft.edit": "Continue Editing",
+ "gui.abuseReport.draft.quittotitle.content": "Would you like to continue editing it or discard it?",
+ "gui.abuseReport.draft.quittotitle.title": "You have a draft chat report that will be lost if you quit",
+ "gui.abuseReport.draft.title": "Edit draft chat report?",
"gui.abuseReport.error.title": "Problem sending your report",
+ "gui.abuseReport.message": "Where did you observe the bad behavior?\nThis will help us in researching your case.",
+ "gui.abuseReport.more_comments": "Please describe what happened:",
+ "gui.abuseReport.name.reporting": "You are reporting \"%s\".",
+ "gui.abuseReport.name.title": "Report Player Name",
+ "gui.abuseReport.observed_what": "Why are you reporting this?",
+ "gui.abuseReport.read_info": "Learn About Reporting",
"gui.abuseReport.reason.alcohol_tobacco_drugs": "Drugs or alcohol",
"gui.abuseReport.reason.alcohol_tobacco_drugs.description": "Someone is encouraging others to partake in illegal drug related activities or encouraging underage drinking.",
"gui.abuseReport.reason.child_sexual_exploitation_or_abuse": "Child sexual exploitation or abuse",
"gui.abuseReport.reason.child_sexual_exploitation_or_abuse.description": "Someone is talking about or otherwise promoting indecent behavior involving children.",
- "gui.abuseReport.reason.defamation_impersonation_false_information": "Defamation, impersonation, or false information",
- "gui.abuseReport.reason.defamation_impersonation_false_information.description": "Someone is damaging someone else's reputation, pretending to be someone they're not, or sharing false information with the aim to exploit or mislead others.",
+ "gui.abuseReport.reason.defamation_impersonation_false_information": "Defamation",
+ "gui.abuseReport.reason.defamation_impersonation_false_information.description": "Someone is damaging your or someone else's reputation, for example sharing false information with the aim to exploit or mislead others.",
"gui.abuseReport.reason.description": "Description:",
"gui.abuseReport.reason.false_reporting": "False Reporting",
+ "gui.abuseReport.reason.generic": "I want to report them",
+ "gui.abuseReport.reason.generic.description": "I'm annoyed with them / they have done something I do not like.",
"gui.abuseReport.reason.harassment_or_bullying": "Harassment or bullying",
"gui.abuseReport.reason.harassment_or_bullying.description": "Someone is shaming, attacking, or bullying you or someone else. This includes when someone is repeatedly trying to contact you or someone else without consent or posting private personal information about you or someone else without consent (\"doxing\").",
"gui.abuseReport.reason.hate_speech": "Hate speech",
"gui.abuseReport.reason.hate_speech.description": "Someone is attacking you or another player based on characteristics of their identity, like religion, race, or sexuality.",
- "gui.abuseReport.reason.imminent_harm": "Imminent harm - Threat to harm others",
+ "gui.abuseReport.reason.imminent_harm": "Threat of harm to others",
"gui.abuseReport.reason.imminent_harm.description": "Someone is threatening to harm you or someone else in real life.",
"gui.abuseReport.reason.narration": "%s: %s",
"gui.abuseReport.reason.non_consensual_intimate_imagery": "Non-consensual intimate imagery",
"gui.abuseReport.reason.non_consensual_intimate_imagery.description": "Someone is talking about, sharing, or otherwise promoting private and intimate images.",
- "gui.abuseReport.reason.self_harm_or_suicide": "Imminent harm - Self-harm or suicide",
+ "gui.abuseReport.reason.self_harm_or_suicide": "Self-harm or suicide",
"gui.abuseReport.reason.self_harm_or_suicide.description": "Someone is threatening to harm themselves in real life or talking about harming themselves in real life.",
"gui.abuseReport.reason.terrorism_or_violent_extremism": "Terrorism or violent extremism",
"gui.abuseReport.reason.terrorism_or_violent_extremism.description": "Someone is talking about, promoting, or threatening to commit acts of terrorism or violent extremism for political, religious, ideological, or other reasons.",
"gui.abuseReport.reason.title": "Select Report Category",
+ "gui.abuseReport.report_sent_msg": "We\u2019ve successfully received your report. Thank you!\n\nOur team will review it as soon as possible.",
+ "gui.abuseReport.select_reason": "Select Report Category",
+ "gui.abuseReport.send": "Send Report",
+ "gui.abuseReport.send.comment_too_long": "Please shorten the comment",
"gui.abuseReport.send.error_message": "An error was returned while sending your report:\n'%s'",
"gui.abuseReport.send.generic_error": "Encountered an unexpected error while sending your report.",
"gui.abuseReport.send.http_error": "An unexpected HTTP error occurred while sending your report.",
"gui.abuseReport.send.json_error": "Encountered malformed payload while sending your report.",
+ "gui.abuseReport.send.no_reason": "Please select a report category",
"gui.abuseReport.send.service_unavailable": "Unable to reach the Abuse Reporting service. Please make sure you are connected to the internet and try again.",
"gui.abuseReport.sending.title": "Sending your report...",
"gui.abuseReport.sent.title": "Report sent",
+ "gui.abuseReport.skin.title": "Report Player Skin",
+ "gui.abuseReport.title": "Report Player",
+ "gui.abuseReport.type.chat": "Chat Messages",
+ "gui.abuseReport.type.name": "Player Name",
+ "gui.abuseReport.type.skin": "Player Skin",
"gui.acknowledge": "Acknowledge",
"gui.advancements": "Advancements",
"gui.all": "All",
"gui.back": "Back",
- "gui.copy_link_to_clipboard": "Copy Link to Clipboard",
"gui.banned.description": "%s\n\n%s\n\nLearn more at the following link: %s",
"gui.banned.description.permanent": "Your account is permanently banned, which means you can\u2019t play online or join Realms.",
"gui.banned.description.reason": "We recently received a report for bad behavior by your account. Our moderators have now reviewed your case and identified it as %s, which goes against the Minecraft Community Standards.",
@@ -3393,6 +3456,8 @@
"gui.banned.description.temporary": "%s Until then, you can\u2019t play online or join Realms.",
"gui.banned.description.temporary.duration": "Your account is temporarily suspended and will be reactivated in %s.",
"gui.banned.description.unknownreason": "We recently received a report for bad behavior by your account. Our moderators have now reviewed your case and identified that it goes against the Minecraft Community Standards.",
+ "gui.banned.name.description": "Your current name - \"%s\" - violates our Community Standards. You can play singleplayer, but will need to change your name to play online.\n\nLearn more or submit a case review at the following link: %s",
+ "gui.banned.name.title": "Name Not Allowed in Multiplayer",
"gui.banned.reason.defamation_impersonation_false_information": "Impersonation or sharing information to exploit or mislead others",
"gui.banned.reason.drugs": "References to illegal drugs",
"gui.banned.reason.extreme_violence_or_gore": "Depictions of real-life excessive violence or gore",
@@ -3406,6 +3471,8 @@
"gui.banned.reason.nudity_or_pornography": "Displaying lewd or pornographic material",
"gui.banned.reason.sexually_inappropriate": "Topics or content of a sexual nature",
"gui.banned.reason.spam_or_advertising": "Spam or advertising",
+ "gui.banned.skin.description": "Your current skin violates our Community Standards. You can still play with a default skin, or select a new one.\n\nLearn more or submit a case review at the following link: %s",
+ "gui.banned.skin.title": "Skin Not Allowed",
"gui.banned.title.permanent": "Account permanently banned",
"gui.banned.title.temporary": "Account temporarily suspended",
"gui.cancel": "Cancel",
@@ -3434,7 +3501,7 @@
"gui.chatReport.send.no_reason": "Please select a report category",
"gui.chatReport.send.no_reported_messages": "Please select at least one chat message to report",
"gui.chatReport.send.too_many_messages": "Trying to include too many messages in the report",
- "gui.chatReport.title": "Report Player",
+ "gui.chatReport.title": "Report Player Chat",
"gui.chatSelection.context": "Messages surrounding this selection will be included to provide additional context",
"gui.chatSelection.fold": "%s message(s) hidden",
"gui.chatSelection.heading": "%s %s",
@@ -3443,6 +3510,7 @@
"gui.chatSelection.selected": "%s/%s message(s) selected",
"gui.chatSelection.title": "Select Chat Messages to Report",
"gui.continue": "Continue",
+ "gui.copy_link_to_clipboard": "Copy Link to Clipboard",
"gui.days": "%s day(s)",
"gui.done": "Done",
"gui.down": "Down",
@@ -3459,6 +3527,7 @@
"gui.ok": "Ok",
"gui.proceed": "Proceed",
"gui.recipebook.moreRecipes": "Right Click for More",
+ "gui.recipebook.page": "%s/%s",
"gui.recipebook.search_hint": "Search...",
"gui.recipebook.toggleRecipes.all": "Showing All",
"gui.recipebook.toggleRecipes.blastable": "Showing Blastable",
@@ -3591,6 +3660,7 @@
"item.minecraft.clay_ball": "Clay Ball",
"item.minecraft.clock": "Clock",
"item.minecraft.coal": "Coal",
+ "item.minecraft.coast_armor_trim_smithing_template": "Smithing Template",
"item.minecraft.cocoa_beans": "Cocoa Beans",
"item.minecraft.cod": "Raw Cod",
"item.minecraft.cod_bucket": "Bucket of Cod",
@@ -3639,6 +3709,7 @@
"item.minecraft.dragon_breath": "Dragon's Breath",
"item.minecraft.dried_kelp": "Dried Kelp",
"item.minecraft.drowned_spawn_egg": "Drowned Spawn Egg",
+ "item.minecraft.dune_armor_trim_smithing_template": "Smithing Template",
"item.minecraft.echo_shard": "Echo Shard",
"item.minecraft.egg": "Egg",
"item.minecraft.elder_guardian_spawn_egg": "Elder Guardian Spawn Egg",
@@ -3656,6 +3727,7 @@
"item.minecraft.experience_bottle": "Bottle o' Enchanting",
"item.minecraft.explorer_pottery_shard": "Explorer Pottery Shard",
"item.minecraft.explorer_pottery_sherd": "Explorer Pottery Sherd",
+ "item.minecraft.eye_armor_trim_smithing_template": "Smithing Template",
"item.minecraft.feather": "Feather",
"item.minecraft.fermented_spider_eye": "Fermented Spider Eye",
"item.minecraft.filled_map": "Map",
@@ -3741,6 +3813,7 @@
"item.minecraft.honeycomb": "Honeycomb",
"item.minecraft.hopper_minecart": "Minecart with Hopper",
"item.minecraft.horse_spawn_egg": "Horse Spawn Egg",
+ "item.minecraft.host_armor_trim_smithing_template": "Smithing Template",
"item.minecraft.howl_pottery_shard": "Howl Pottery Shard",
"item.minecraft.howl_pottery_sherd": "Howl Pottery Sherd",
"item.minecraft.husk_spawn_egg": "Husk Spawn Egg",
@@ -3867,6 +3940,7 @@
"item.minecraft.netherite_scrap": "Netherite Scrap",
"item.minecraft.netherite_shovel": "Netherite Shovel",
"item.minecraft.netherite_sword": "Netherite Sword",
+ "item.minecraft.netherite_upgrade_smithing_template": "Smithing Template",
"item.minecraft.oak_boat": "Oak Boat",
"item.minecraft.oak_chest_boat": "Oak Boat with Chest",
"item.minecraft.ocelot_spawn_egg": "Ocelot Spawn Egg",
@@ -3937,6 +4011,7 @@
"item.minecraft.rabbit_hide": "Rabbit Hide",
"item.minecraft.rabbit_spawn_egg": "Rabbit Spawn Egg",
"item.minecraft.rabbit_stew": "Rabbit Stew",
+ "item.minecraft.raiser_armor_trim_smithing_template": "Smithing Template",
"item.minecraft.ravager_spawn_egg": "Ravager Spawn Egg",
"item.minecraft.raw_copper": "Raw Copper",
"item.minecraft.raw_gold": "Raw Gold",
@@ -3944,12 +4019,15 @@
"item.minecraft.recovery_compass": "Recovery Compass",
"item.minecraft.red_dye": "Red Dye",
"item.minecraft.redstone": "Redstone Dust",
+ "item.minecraft.rib_armor_trim_smithing_template": "Smithing Template",
"item.minecraft.rotten_flesh": "Rotten Flesh",
"item.minecraft.saddle": "Saddle",
"item.minecraft.salmon": "Raw Salmon",
"item.minecraft.salmon_bucket": "Bucket of Salmon",
"item.minecraft.salmon_spawn_egg": "Salmon Spawn Egg",
"item.minecraft.scute": "Scute",
+ "item.minecraft.sentry_armor_trim_smithing_template": "Smithing Template",
+ "item.minecraft.shaper_armor_trim_smithing_template": "Smithing Template",
"item.minecraft.sheaf_pottery_shard": "Sheaf Pottery Shard",
"item.minecraft.sheaf_pottery_sherd": "Sheaf Pottery Sherd",
"item.minecraft.shears": "Shears",
@@ -3976,6 +4054,7 @@
"item.minecraft.shulker_shell": "Shulker Shell",
"item.minecraft.shulker_spawn_egg": "Shulker Spawn Egg",
"item.minecraft.sign": "Sign",
+ "item.minecraft.silence_armor_trim_smithing_template": "Smithing Template",
"item.minecraft.silverfish_spawn_egg": "Silverfish Spawn Egg",
"item.minecraft.skeleton_horse_spawn_egg": "Skeleton Horse Spawn Egg",
"item.minecraft.skeleton_spawn_egg": "Skeleton Spawn Egg",
@@ -4000,11 +4079,13 @@
"item.minecraft.sniffer_spawn_egg": "Sniffer Spawn Egg",
"item.minecraft.snort_pottery_shard": "Snort Pottery Shard",
"item.minecraft.snort_pottery_sherd": "Snort Pottery Sherd",
+ "item.minecraft.snout_armor_trim_smithing_template": "Smithing Template",
"item.minecraft.snow_golem_spawn_egg": "Snow Golem Spawn Egg",
"item.minecraft.snowball": "Snowball",
"item.minecraft.spectral_arrow": "Spectral Arrow",
"item.minecraft.spider_eye": "Spider Eye",
"item.minecraft.spider_spawn_egg": "Spider Spawn Egg",
+ "item.minecraft.spire_armor_trim_smithing_template": "Smithing Template",
"item.minecraft.splash_potion": "Splash Potion",
"item.minecraft.splash_potion.effect.awkward": "Awkward Splash Potion",
"item.minecraft.splash_potion.effect.empty": "Splash Uncraftable Potion",
@@ -4046,6 +4127,7 @@
"item.minecraft.sweet_berries": "Sweet Berries",
"item.minecraft.tadpole_bucket": "Bucket of Tadpole",
"item.minecraft.tadpole_spawn_egg": "Tadpole Spawn Egg",
+ "item.minecraft.tide_armor_trim_smithing_template": "Smithing Template",
"item.minecraft.tipped_arrow": "Tipped Arrow",
"item.minecraft.tipped_arrow.effect.awkward": "Tipped Arrow",
"item.minecraft.tipped_arrow.effect.empty": "Uncraftable Tipped Arrow",
@@ -4079,16 +4161,20 @@
"item.minecraft.tropical_fish_spawn_egg": "Tropical Fish Spawn Egg",
"item.minecraft.turtle_helmet": "Turtle Shell",
"item.minecraft.turtle_spawn_egg": "Turtle Spawn Egg",
+ "item.minecraft.vex_armor_trim_smithing_template": "Smithing Template",
"item.minecraft.vex_spawn_egg": "Vex Spawn Egg",
"item.minecraft.villager_spawn_egg": "Villager Spawn Egg",
"item.minecraft.vindicator_spawn_egg": "Vindicator Spawn Egg",
"item.minecraft.wandering_trader_spawn_egg": "Wandering Trader Spawn Egg",
+ "item.minecraft.ward_armor_trim_smithing_template": "Smithing Template",
"item.minecraft.warden_spawn_egg": "Warden Spawn Egg",
"item.minecraft.warped_fungus_on_a_stick": "Warped Fungus on a Stick",
"item.minecraft.water_bucket": "Water Bucket",
+ "item.minecraft.wayfinder_armor_trim_smithing_template": "Smithing Template",
"item.minecraft.wheat": "Wheat",
"item.minecraft.wheat_seeds": "Wheat Seeds",
"item.minecraft.white_dye": "White Dye",
+ "item.minecraft.wild_armor_trim_smithing_template": "Smithing Template",
"item.minecraft.witch_spawn_egg": "Witch Spawn Egg",
"item.minecraft.wither_skeleton_spawn_egg": "Wither Skeleton Spawn Egg",
"item.minecraft.wither_spawn_egg": "Wither Spawn Egg",
@@ -4283,6 +4369,7 @@
"lanServer.start": "Start LAN World",
"lanServer.title": "LAN World",
"lectern.take_book": "Take Book",
+ "loading.progress": "%s%%",
"mco.account.privacy.info": "Read more about Mojang and privacy laws",
"mco.account.privacyinfo": "Mojang implements certain procedures to help protect children and their privacy including complying with the Children\u2019s Online Privacy Protection Act (COPPA) and General Data Protection Regulation (GDPR).\n\nYou may need to obtain parental consent before accessing your Realms account.\n\nIf you have an older Minecraft account (you log in with your username), you need to migrate the account to a Mojang account in order to access Realms.",
"mco.account.update": "Update account",
@@ -4395,9 +4482,6 @@
"mco.configure.world.spawnNPCs": "Spawn NPCs",
"mco.configure.world.spawnProtection": "Spawn protection",
"mco.configure.world.status": "Status",
- "mco.configure.world.subscription.remaining.months.days": "%1$s month(s), %2$s day(s)",
- "mco.configure.world.subscription.remaining.months": "%1$s month(s)",
- "mco.configure.world.subscription.remaining.days": "%1$s day(s)",
"mco.configure.world.subscription.day": "day",
"mco.configure.world.subscription.days": "days",
"mco.configure.world.subscription.expired": "Expired",
@@ -4407,6 +4491,9 @@
"mco.configure.world.subscription.months": "months",
"mco.configure.world.subscription.recurring.daysleft": "Renewed automatically in",
"mco.configure.world.subscription.recurring.info": "Changes made to your Realms subscription such as stacking time or turning off recurring billing will not be reflected until your next bill date.",
+ "mco.configure.world.subscription.remaining.days": "%1$s day(s)",
+ "mco.configure.world.subscription.remaining.months": "%1$s month(s)",
+ "mco.configure.world.subscription.remaining.months.days": "%1$s month(s), %2$s day(s)",
"mco.configure.world.subscription.start": "Start date",
"mco.configure.world.subscription.timeleft": "Time left",
"mco.configure.world.subscription.title": "Your subscription",
@@ -4414,8 +4501,8 @@
"mco.configure.world.switch.slot": "Create world",
"mco.configure.world.switch.slot.subtitle": "This world is empty, choose how to create your world",
"mco.configure.world.title": "Configure realm:",
- "mco.configure.world.uninvite.question": "Are you sure that you want to uninvite",
"mco.configure.world.uninvite.player": "Are you sure that you want to uninvite '%s'?",
+ "mco.configure.world.uninvite.question": "Are you sure that you want to uninvite",
"mco.configure.worlds.title": "Worlds",
"mco.connect.authorizing": "Logging in...",
"mco.connect.connecting": "Connecting to the realm...",
@@ -4438,6 +4525,7 @@
"mco.download.preparing": "Preparing download",
"mco.download.resourcePack.fail": "Failed to download resource pack!",
"mco.download.speed": "(%s/s)",
+ "mco.download.speed.narration": "%s/s",
"mco.download.title": "Downloading latest world",
"mco.error.invalid.session.message": "Please try restarting Minecraft",
"mco.error.invalid.session.title": "Invalid session",
@@ -4452,8 +4540,12 @@
"mco.errorMessage.6009": "Invalid Realm description",
"mco.errorMessage.connectionFailure": "An error occurred, please try again later.",
"mco.errorMessage.generic": "An error occurred: ",
+ "mco.errorMessage.noDetails": "No error details provided",
"mco.errorMessage.realmsService": "An error occurred (%s):",
+ "mco.errorMessage.realmsService.connectivity": "Could not connect to Realms: %s",
"mco.errorMessage.realmsService.realmsError": "Realms (%s):",
+ "mco.errorMessage.realmsService.unknownCompatibility": "Could not check compatible version, got response: %s",
+ "mco.errorMessage.retry": "Retry operation",
"mco.errorMessage.serviceBusy": "Realms is busy at the moment.\nPlease try connecting to your Realm again in a couple of minutes.",
"mco.gui.button": "Button",
"mco.gui.ok": "Ok",
@@ -4500,6 +4592,7 @@
"mco.selectServer.configure": "Configure",
"mco.selectServer.configureRealm": "Configure realm",
"mco.selectServer.create": "Create realm",
+ "mco.selectServer.create.subtitle": "Select what world to put on your new realm",
"mco.selectServer.expired": "Expired realm",
"mco.selectServer.expiredList": "Your subscription has expired",
"mco.selectServer.expiredRenew": "Renew",
@@ -4509,14 +4602,16 @@
"mco.selectServer.expires.days": "Expires in %s days",
"mco.selectServer.expires.soon": "Expires soon",
"mco.selectServer.leave": "Leave realm",
+ "mco.selectServer.loading": "Loading Realms List",
"mco.selectServer.mapOnlySupportedForVersion": "This map is unsupported in %s",
"mco.selectServer.minigame": "Minigame:",
"mco.selectServer.minigameNotSupportedInVersion": "Can't play this minigame in %s",
+ "mco.selectServer.noRealms": "You don't seem to have a Realm. Add a Realm to play together with your friends.",
"mco.selectServer.note": "Note:",
"mco.selectServer.open": "Open realm",
"mco.selectServer.openserver": "Open realm",
"mco.selectServer.play": "Play",
- "mco.selectServer.popup": "Realms is a safe, simple way to enjoy an online Minecraft world with up to ten friends at a time. It supports loads of minigames and plenty of custom worlds! Only the owner of the realm needs to pay.",
+ "mco.selectServer.popup": "Realms is a safe, simple way to enjoy an online Minecraft world with up to ten friends at a time. It supports loads of minigames and plenty of custom worlds! Only the owner of the realm needs to pay.",
"mco.selectServer.purchase": "Add Realm",
"mco.selectServer.trial": "Get a trial!",
"mco.selectServer.uninitialized": "Click to start your new realm!",
@@ -4539,16 +4634,22 @@
"mco.terms.sentence.1": "I agree to the Minecraft Realms",
"mco.terms.sentence.2": "Terms of Service",
"mco.terms.title": "Realms Terms of Service",
+ "mco.time.daysAgo": "%1$s day(s) ago",
+ "mco.time.hoursAgo": "%1$s hour(s) ago",
+ "mco.time.minutesAgo": "%1$s minute(s) ago",
+ "mco.time.now": "right now",
+ "mco.time.secondsAgo": "%1$s second(s) ago",
"mco.trial.message.line1": "Want to get your own realm?",
"mco.trial.message.line2": "Click here for more info!",
"mco.upload.button.name": "Upload",
"mco.upload.cancelled": "Upload cancelled",
"mco.upload.close.failure": "Could not close your realm, please try again later",
"mco.upload.done": "Upload done",
- "mco.upload.entry.id": "%1$s (%2$s)",
"mco.upload.entry.cheats": "%1$s, %2$s",
+ "mco.upload.entry.id": "%1$s (%2$s)",
"mco.upload.failed": "Upload failed! (%s)",
"mco.upload.hardcore": "Hardcore worlds can't be uploaded!",
+ "mco.upload.percent": "%s %%",
"mco.upload.preparing": "Preparing your world",
"mco.upload.select.world.none": "No singleplayer worlds found!",
"mco.upload.select.world.subtitle": "Please select a singleplayer world to upload",
@@ -4557,11 +4658,6 @@
"mco.upload.size.failure.line2": "It is %s. The maximum allowed size is %s.",
"mco.upload.uploading": "Uploading '%s'",
"mco.upload.verifying": "Verifying your world",
- "mco.time.now": "right now",
- "mco.time.secondsAgo": "%1$s second(s) ago",
- "mco.time.minutesAgo": "%1$s minute(s) ago",
- "mco.time.hoursAgo": "%1$s hour(s) ago",
- "mco.time.daysAgo": "%1$s day(s) ago",
"mco.warning": "Warning!",
"mco.worldSlot.minigame": "Minigame",
"menu.convertingLevel": "Converting world",
@@ -4599,6 +4695,7 @@
"merchant.level.4": "Expert",
"merchant.level.5": "Master",
"merchant.next_level": "Trader's next level",
+ "merchant.title": "%s - %s",
"merchant.trades": "Trades",
"mirror.front_back": "\u2191 \u2193",
"mirror.left_right": "\u2190 \u2192",
@@ -4648,6 +4745,7 @@
"multiplayer.player.joined": "%s joined the game",
"multiplayer.player.joined.renamed": "%s (formerly known as %s) joined the game",
"multiplayer.player.left": "%s left the game",
+ "multiplayer.player.list.hp": "%shp",
"multiplayer.player.list.narration": "Online players: %s",
"multiplayer.requiredTexturePrompt.disconnect": "Server requires a custom resource pack",
"multiplayer.requiredTexturePrompt.line1": "This server requires the use of a custom resource pack.",
@@ -4666,6 +4764,7 @@
"multiplayer.status.ping": "%s ms",
"multiplayer.status.ping.narration": "Ping %s milliseconds",
"multiplayer.status.pinging": "Pinging...",
+ "multiplayer.status.player_count": "%s/%s",
"multiplayer.status.player_count.narration": "%s out of %s players online",
"multiplayer.status.quitting": "Quitting",
"multiplayer.status.request_handled": "Status request has been handled",
@@ -4702,6 +4801,10 @@
"narration.slider.usage.hovered": "Drag slider to change value",
"narration.suggestion": "Selected suggestion %d out of %d: %s",
"narration.suggestion.tooltip": "Selected suggestion %d out of %d: %s (%s)",
+ "narration.suggestion.usage.cycle.fixed": "Press Tab to cycle to the next suggestion",
+ "narration.suggestion.usage.cycle.hidable": "Press Tab to cycle to the next suggestion, or Escape to leave suggestions",
+ "narration.suggestion.usage.fill.fixed": "Press Tab to use suggestion",
+ "narration.suggestion.usage.fill.hidable": "Press Tab to use suggestion, or Escape to leave suggestions",
"narration.tab_navigation.usage": "Press Ctrl and Tab to switch between tabs",
"narrator.button.accessibility": "Accessibility",
"narrator.button.difficulty_lock": "Difficulty lock",
@@ -4731,21 +4834,26 @@
"optimizeWorld.info.converted": "Upgraded chunks: %s",
"optimizeWorld.info.skipped": "Skipped chunks: %s",
"optimizeWorld.info.total": "Total chunks: %s",
+ "optimizeWorld.progress.counter": "%s / %s",
+ "optimizeWorld.progress.percentage": "%s%%",
"optimizeWorld.stage.counting": "Counting chunks...",
"optimizeWorld.stage.failed": "Failed! :(",
"optimizeWorld.stage.finished": "Finishing up...",
"optimizeWorld.stage.upgrading": "Upgrading all chunks...",
"optimizeWorld.title": "Optimizing World '%s'",
+ "options.accessibility": "Accessibility Settings...",
"options.accessibility.high_contrast": "High Contrast",
"options.accessibility.high_contrast.error.tooltip": "High Contrast resource pack is not available",
"options.accessibility.high_contrast.tooltip": "Enhances the contrast of UI elements",
"options.accessibility.link": "Accessibility Guide",
+ "options.accessibility.narrator_hotkey": "Narrator Hotkey",
+ "options.accessibility.narrator_hotkey.tooltip": "Allows the Narrator to be toggled on and off with 'Ctrl+B'",
"options.accessibility.panorama_speed": "Panorama Scroll Speed",
"options.accessibility.text_background": "Text Background",
"options.accessibility.text_background_opacity": "Text Background Opacity",
"options.accessibility.text_background.chat": "Chat",
"options.accessibility.text_background.everywhere": "Everywhere",
- "options.accessibility.title": "Accessibility Settings...",
+ "options.accessibility.title": "Accessibility Settings",
"options.allowServerListing": "Allow Server Listings",
"options.allowServerListing.tooltip": "Servers may list online players as part of their public status.\nWith this option off your name will not show up in such lists.",
"options.ao": "Smooth Lighting",
@@ -4769,6 +4877,7 @@
"options.biomeBlendRadius.11": "11x11 (Extreme)",
"options.biomeBlendRadius.13": "13x13 (Showoff)",
"options.biomeBlendRadius.15": "15x15 (Maximum)",
+ "options.chat": "Chat Settings...",
"options.chat.color": "Colors",
"options.chat.delay": "Chat Delay: %s seconds",
"options.chat.delay_none": "Chat Delay: None",
@@ -4779,7 +4888,7 @@
"options.chat.links.prompt": "Prompt on Links",
"options.chat.opacity": "Chat Text Opacity",
"options.chat.scale": "Chat Text Size",
- "options.chat.title": "Chat Settings...",
+ "options.chat.title": "Chat Settings",
"options.chat.visibility": "Chat",
"options.chat.visibility.full": "Shown",
"options.chat.visibility.hidden": "Hidden",
@@ -4825,6 +4934,7 @@
"options.framerateLimit.max": "Unlimited",
"options.fullscreen": "Fullscreen",
"options.fullscreen.current": "Current",
+ "options.fullscreen.entry": "%sx%s@%s (%sbit)",
"options.fullscreen.resolution": "Fullscreen Resolution",
"options.fullscreen.unavailable": "Setting unavailable",
"options.gamma": "Brightness",
@@ -4861,6 +4971,8 @@
"options.key.hold": "Hold",
"options.key.toggle": "Toggle",
"options.language": "Language...",
+ "options.language.title": "Language",
+ "options.languageAccuracyWarning": "(Language translations may not be 100%% accurate)",
"options.languageWarning": "Language translations may not be 100%% accurate",
"options.mainHand": "Main Hand",
"options.mainHand.left": "Left",
@@ -4939,12 +5051,14 @@
"options.viewBobbing": "View Bobbing",
"options.visible": "Shown",
"options.vsync": "VSync",
- "outOfMemory.message": "Minecraft has run out of memory.\n\nThis could be caused by a bug in the game or by the Java Virtual Machine not being allocated enough memory.\n\nTo prevent level corruption, the current game has quit. We've tried to free up enough memory to let you go back to the main menu and back to playing, but this may not have worked.\n\nPlease restart the game if you see this message again.",
+ "outOfMemory.message": "Minecraft has run out of memory.\n\nThis could be caused by a bug in the game or by the Java Virtual Machine not being allocated enough memory.\n\nTo prevent world corruption, the current game has quit. We've tried to free up enough memory to let you go back to the main menu and back to playing, but this may not have worked.\n\nPlease restart the game if you see this message again.",
"outOfMemory.title": "Out of memory!",
"pack.available.title": "Available",
"pack.copyFailure": "Failed to copy packs",
"pack.dropConfirm": "Do you want to add the following packs to Minecraft?",
"pack.dropInfo": "Drag and drop files into this window to add packs",
+ "pack.dropRejected.message": "The following entries were not valid packs and were not copied:\n %s",
+ "pack.dropRejected.title": "Non-pack entries",
"pack.folderInfo": "(Place pack files here)",
"pack.incompatible": "Incompatible",
"pack.incompatible.confirm.new": "This pack was made for a newer version of Minecraft and may not work correctly.",
@@ -5165,7 +5279,7 @@
"selectWorld.import_worldgen_settings.select_file": "Select settings file (.json)",
"selectWorld.incompatible_series": "Created by an incompatible version",
"selectWorld.load_folder_access": "Unable to read or access folder where game worlds are saved!",
- "selectWorld.loading_list": "Loading world list",
+ "selectWorld.loading_list": "Loading World List",
"selectWorld.locked": "Locked by another running instance of Minecraft",
"selectWorld.mapFeatures": "Generate Structures",
"selectWorld.mapFeatures.info": "Villages, Shipwrecks, etc.",
@@ -5313,6 +5427,7 @@
"stat.minecraft.walk_one_cm": "Distance Walked",
"stat.minecraft.walk_under_water_one_cm": "Distance Walked under Water",
"stat.mobsButton": "Mobs",
+ "stats.none": "-",
"stats.tooltip.type.statistic": "Statistic",
"structure_block.button.detect_size": "DETECT",
"structure_block.button.load": "LOAD",
@@ -5454,6 +5569,7 @@
"subtitles.block.sniffer_egg.crack": "Sniffer Egg cracks",
"subtitles.block.sniffer_egg.hatch": "Sniffer Egg hatches",
"subtitles.block.sniffer_egg.plop": "Sniffer plops",
+ "subtitles.block.sponge.absorb": "Sponge sucks",
"subtitles.block.sweet_berry_bush.pick_berries": "Berries pop",
"subtitles.block.trapdoor.toggle": "Trapdoor creaks",
"subtitles.block.tripwire.attach": "Tripwire attaches",
@@ -6094,8 +6210,12 @@
"subtitles.ui.loom.take_result": "Loom used",
"subtitles.ui.stonecutter.take_result": "Stonecutter used",
"subtitles.weather.rain": "Rain falls",
- "symlink_warning.title": "World folder contains symbolic links",
"symlink_warning.message": "Loading worlds from folders with symbolic links can be unsafe if you don't know exactly what you are doing. Please visit %s to learn more.",
+ "symlink_warning.message.pack": "Loading packs with symbolic links can be unsafe if you don't know exactly what you are doing. Please visit %s to learn more.",
+ "symlink_warning.message.world": "Loading worlds from folders with symbolic links can be unsafe if you don't know exactly what you are doing. Please visit %s to learn more.",
+ "symlink_warning.title": "World folder contains symbolic links",
+ "symlink_warning.title.pack": "Added pack(s) contain(s) symbolic links",
+ "symlink_warning.title.world": "The world folder contains symbolic links",
"team.collision.always": "Always",
"team.collision.never": "Never",
"team.collision.pushOtherTeams": "Push other teams",
@@ -6106,6 +6226,7 @@
"team.visibility.hideForOwnTeam": "Hide for own team",
"team.visibility.never": "Never",
"telemetry_info.button.give_feedback": "Give Feedback",
+ "telemetry_info.button.privacy_statement": "Privacy Statement",
"telemetry_info.button.show_data": "Open My Data",
"telemetry_info.property_title": "Included Data",
"telemetry_info.screen.description": "Collecting this data helps us improve Minecraft by guiding us in directions that are relevant to our players.\nYou can also send in additional feedback to help us keep improving Minecraft.",
@@ -6159,7 +6280,9 @@
"title.32bit.deprecation.realms": "Minecraft will soon require a 64-bit system, which will prevent you from playing or using Realms on this device. You will need to manually cancel any Realms subscription.",
"title.32bit.deprecation.realms.check": "Do not show this screen again",
"title.32bit.deprecation.realms.header": "32-bit system detected",
+ "title.credits": "Copyright Mojang AB. Do not distribute!",
"title.multiplayer.disabled": "Multiplayer is disabled. Please check your Microsoft account settings.",
+ "title.multiplayer.disabled.banned.name": "You must change your name before you can play online",
"title.multiplayer.disabled.banned.permanent": "Your account is permanently suspended from online play",
"title.multiplayer.disabled.banned.temporary": "Your account is temporarily suspended from online play",
"title.multiplayer.lan": "Multiplayer (LAN)",
diff --git a/azalea-nbt/src/decode.rs b/azalea-nbt/src/decode.rs
index 35392b15..b2cc8c77 100755
--- a/azalea-nbt/src/decode.rs
+++ b/azalea-nbt/src/decode.rs
@@ -255,6 +255,9 @@ impl Nbt {
}
/// Read the NBT data. This will return a compound tag with a single item.
+ ///
+ /// Minecraft usually uses this function when reading from files.
+ /// [`Nbt::read_any_tag`] is used when reading from the network.
pub fn read(stream: &mut Cursor<&[u8]>) -> Result {
// default to compound tag
@@ -271,6 +274,17 @@ impl Nbt {
Ok(Nbt::Compound(map))
}
+ /// Read the NBT data. There is no guarantee that the tag will be a compound
+ /// with a single item.
+ ///
+ /// The Minecraft protocol uses this function when reading from the network.
+ /// [`Nbt::read`] is usually used when reading from files.
+ pub fn read_any_tag(stream: &mut Cursor<&[u8]>) -> Result {
+ let tag_id = stream.read_u8().unwrap_or(0);
+ let tag = Nbt::read_known(stream, tag_id)?;
+ Ok(tag)
+ }
+
/// Read the NBT data compressed wtih zlib.
pub fn read_zlib(stream: &mut impl BufRead) -> Result {
let mut gz = ZlibDecoder::new(stream);
@@ -290,7 +304,7 @@ impl Nbt {
impl McBufReadable for Nbt {
fn read_from(buf: &mut Cursor<&[u8]>) -> Result {
- Ok(Nbt::read(buf)?)
+ Ok(Nbt::read_any_tag(buf)?)
}
}
impl From for BufReadError {
diff --git a/azalea-nbt/src/encode.rs b/azalea-nbt/src/encode.rs
index 76b70b6e..34c451d2 100755
--- a/azalea-nbt/src/encode.rs
+++ b/azalea-nbt/src/encode.rs
@@ -16,51 +16,55 @@ fn write_compound(writer: &mut impl Write, value: &NbtCompound, end_tag: bool) {
for (key, tag) in value.iter() {
writer.write_u8(tag.id()).unwrap();
write_string(writer, key);
- match tag {
- Nbt::End => {}
- Nbt::Byte(value) => {
- writer.write_i8(*value).unwrap();
- }
- Nbt::Short(value) => {
- writer.write_i16::(*value).unwrap();
- }
- Nbt::Int(value) => {
- writer.write_i32::(*value).unwrap();
- }
- Nbt::Long(value) => {
- writer.write_i64::(*value).unwrap();
- }
- Nbt::Float(value) => {
- writer.write_f32::(*value).unwrap();
- }
- Nbt::Double(value) => {
- writer.write_f64::(*value).unwrap();
- }
- Nbt::ByteArray(value) => {
- write_byte_array(writer, value);
- }
- Nbt::String(value) => {
- write_string(writer, value);
- }
- Nbt::List(value) => {
- write_list(writer, value);
- }
- Nbt::Compound(value) => {
- write_compound(writer, value, true);
- }
- Nbt::IntArray(value) => {
- write_int_array(writer, value);
- }
- Nbt::LongArray(value) => {
- write_long_array(writer, value);
- }
- }
+ write_known(writer, tag);
}
if end_tag {
writer.write_u8(END_ID).unwrap();
}
}
+fn write_known(writer: &mut impl Write, tag: &Nbt) {
+ match tag {
+ Nbt::End => {}
+ Nbt::Byte(value) => {
+ writer.write_i8(*value).unwrap();
+ }
+ Nbt::Short(value) => {
+ writer.write_i16::(*value).unwrap();
+ }
+ Nbt::Int(value) => {
+ writer.write_i32::(*value).unwrap();
+ }
+ Nbt::Long(value) => {
+ writer.write_i64::(*value).unwrap();
+ }
+ Nbt::Float(value) => {
+ writer.write_f32::(*value).unwrap();
+ }
+ Nbt::Double(value) => {
+ writer.write_f64::(*value).unwrap();
+ }
+ Nbt::ByteArray(value) => {
+ write_byte_array(writer, value);
+ }
+ Nbt::String(value) => {
+ write_string(writer, value);
+ }
+ Nbt::List(value) => {
+ write_list(writer, value);
+ }
+ Nbt::Compound(value) => {
+ write_compound(writer, value, true);
+ }
+ Nbt::IntArray(value) => {
+ write_int_array(writer, value);
+ }
+ Nbt::LongArray(value) => {
+ write_long_array(writer, value);
+ }
+ }
+}
+
#[inline]
fn write_list(writer: &mut impl Write, value: &NbtList) {
writer.write_u8(value.id()).unwrap();
@@ -256,6 +260,13 @@ impl Nbt {
}
}
+ /// Write any tag as NBT data. This is used by Minecraft when writing to the
+ /// network, otherwise [`Nbt::write`] is usually used instead.
+ pub fn write_any(&self, writer: &mut impl Write) {
+ writer.write_u8(self.id()).unwrap();
+ write_known(writer, self);
+ }
+
/// Write the compound tag as NBT data compressed wtih zlib.
///
/// # Errors
@@ -279,7 +290,7 @@ impl Nbt {
impl McBufWritable for Nbt {
fn write_into(&self, buf: &mut impl Write) -> Result<(), std::io::Error> {
- self.write(buf);
+ self.write_any(buf);
Ok(())
}
}
diff --git a/azalea-nbt/src/tag.rs b/azalea-nbt/src/tag.rs
index efb6bdd2..224db2d3 100755
--- a/azalea-nbt/src/tag.rs
+++ b/azalea-nbt/src/tag.rs
@@ -226,6 +226,16 @@ impl NbtCompound {
self.inner.len() >= 32
}
}
+
+impl IntoIterator for NbtCompound {
+ type Item = (NbtString, Nbt);
+ type IntoIter = std::vec::IntoIter;
+
+ fn into_iter(self) -> Self::IntoIter {
+ self.inner.into_iter()
+ }
+}
+
#[cfg(feature = "serde")]
impl Serialize for NbtCompound {
fn serialize(&self, serializer: S) -> Result {
diff --git a/azalea-protocol/azalea-protocol-macros/src/lib.rs b/azalea-protocol/azalea-protocol-macros/src/lib.rs
index e04a2dbc..ffecc13a 100755
--- a/azalea-protocol/azalea-protocol-macros/src/lib.rs
+++ b/azalea-protocol/azalea-protocol-macros/src/lib.rs
@@ -47,7 +47,7 @@ pub fn derive_serverbound_game_packet(input: TokenStream) -> TokenStream {
pub fn derive_serverbound_handshake_packet(input: TokenStream) -> TokenStream {
as_packet_derive(
input,
- quote! {crate::packets::handshake::ServerboundHandshakePacket},
+ quote! {crate::packets::handshaking::ServerboundHandshakePacket},
)
}
#[proc_macro_derive(ServerboundLoginPacket, attributes(var))]
@@ -64,6 +64,13 @@ pub fn derive_serverbound_status_packet(input: TokenStream) -> TokenStream {
quote! {crate::packets::status::ServerboundStatusPacket},
)
}
+#[proc_macro_derive(ServerboundConfigurationPacket, attributes(var))]
+pub fn derive_serverbound_configuration_packet(input: TokenStream) -> TokenStream {
+ as_packet_derive(
+ input,
+ quote! {crate::packets::configuration::ServerboundConfigurationPacket},
+ )
+}
#[proc_macro_derive(ClientboundGamePacket, attributes(var))]
pub fn derive_clientbound_game_packet(input: TokenStream) -> TokenStream {
@@ -73,7 +80,7 @@ pub fn derive_clientbound_game_packet(input: TokenStream) -> TokenStream {
pub fn derive_clientbound_handshake_packet(input: TokenStream) -> TokenStream {
as_packet_derive(
input,
- quote! {crate::packets::handshake::ClientboundHandshakePacket},
+ quote! {crate::packets::handshaking::ClientboundHandshakePacket},
)
}
#[proc_macro_derive(ClientboundLoginPacket, attributes(var))]
@@ -90,6 +97,13 @@ pub fn derive_clientbound_status_packet(input: TokenStream) -> TokenStream {
quote! {crate::packets::status::ClientboundStatusPacket},
)
}
+#[proc_macro_derive(ClientboundConfigurationPacket, attributes(var))]
+pub fn derive_clientbound_configuration_packet(input: TokenStream) -> TokenStream {
+ as_packet_derive(
+ input,
+ quote! {crate::packets::configuration::ClientboundConfigurationPacket},
+ )
+}
#[derive(Debug)]
struct PacketIdPair {
diff --git a/azalea-protocol/examples/handshake_proxy.rs b/azalea-protocol/examples/handshake_proxy.rs
index 34e9553f..f7fb0f5c 100644
--- a/azalea-protocol/examples/handshake_proxy.rs
+++ b/azalea-protocol/examples/handshake_proxy.rs
@@ -4,7 +4,7 @@
use azalea_protocol::{
connect::Connection,
packets::{
- handshake::{
+ handshaking::{
client_intention_packet::ClientIntentionPacket, ClientboundHandshakePacket,
ServerboundHandshakePacket,
},
@@ -145,11 +145,7 @@ async fn handle_connection(stream: TcpStream) -> anyhow::Result<()> {
"Player \'{0}\' from {1} logging in with uuid: {2}",
hello.name,
ip.ip(),
- if let Some(id) = hello.profile_id {
- id.to_string()
- } else {
- String::new()
- }
+ hello.profile_id.to_string()
);
tokio::spawn(transfer(conn.unwrap()?, intent, hello).map(|r| {
diff --git a/azalea-protocol/src/connect.rs b/azalea-protocol/src/connect.rs
index 5df1d874..9c573506 100755
--- a/azalea-protocol/src/connect.rs
+++ b/azalea-protocol/src/connect.rs
@@ -1,19 +1,23 @@
//! Connect to remote servers/clients.
+use crate::packets::configuration::{
+ ClientboundConfigurationPacket, ServerboundConfigurationPacket,
+};
use crate::packets::game::{ClientboundGamePacket, ServerboundGamePacket};
-use crate::packets::handshake::{ClientboundHandshakePacket, ServerboundHandshakePacket};
+use crate::packets::handshaking::{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, try_read_packet, ReadPacketError};
-use crate::write::write_packet;
+use crate::read::{deserialize_packet, read_raw_packet, try_read_raw_packet, ReadPacketError};
+use crate::write::{serialize_packet, write_raw_packet};
use azalea_auth::game_profile::GameProfile;
use azalea_auth::sessionserver::{ClientSessionServerError, ServerSessionServerError};
use azalea_crypto::{Aes128CfbDec, Aes128CfbEnc};
use bytes::BytesMut;
use log::{error, info};
use std::fmt::Debug;
+use std::io::Cursor;
use std::marker::PhantomData;
use std::net::SocketAddr;
use thiserror::Error;
@@ -22,20 +26,28 @@ use tokio::net::tcp::{OwnedReadHalf, OwnedWriteHalf, ReuniteError};
use tokio::net::TcpStream;
use uuid::Uuid;
-/// The read half of a connection.
-pub struct ReadConnection {
+pub struct RawReadConnection {
pub read_stream: OwnedReadHalf,
pub buffer: BytesMut,
pub compression_threshold: Option,
pub dec_cipher: Option,
+}
+
+pub struct RawWriteConnection {
+ pub write_stream: OwnedWriteHalf,
+ pub compression_threshold: Option,
+ pub enc_cipher: Option,
+}
+
+/// The read half of a connection.
+pub struct ReadConnection {
+ pub raw: RawReadConnection,
_reading: PhantomData,
}
/// The write half of a connection.
pub struct WriteConnection {
- pub write_stream: OwnedWriteHalf,
- pub compression_threshold: Option,
- pub enc_cipher: Option,
+ pub raw: RawWriteConnection,
_writing: PhantomData,
}
@@ -55,7 +67,7 @@ pub struct WriteConnection {
/// serverbound_hello_packet::ServerboundHelloPacket,
/// serverbound_key_packet::ServerboundKeyPacket
/// },
-/// handshake::client_intention_packet::ClientIntentionPacket
+/// handshaking::client_intention_packet::ClientIntentionPacket
/// }
/// };
///
@@ -82,7 +94,7 @@ pub struct WriteConnection {
/// conn.write(
/// ServerboundHelloPacket {
/// name: "bot".to_string(),
-/// profile_id: None,
+/// profile_id: uuid::Uuid::nil(),
/// }
/// .get(),
/// )
@@ -108,7 +120,7 @@ pub struct WriteConnection {
/// conn.set_compression_threshold(p.compression_threshold);
/// }
/// ClientboundLoginPacket::GameProfile(p) => {
-/// break (conn.game(), p.game_profile);
+/// break (conn.configuration(), p.game_profile);
/// }
/// ClientboundLoginPacket::LoginDisconnect(p) => {
/// eprintln!("login disconnect: {}", p.reason);
@@ -126,13 +138,9 @@ pub struct Connection {
pub writer: WriteConnection,
}
-impl ReadConnection
-where
- R: ProtocolPacket + Debug,
-{
- /// Read a packet from the stream.
- pub async fn read(&mut self) -> Result> {
- read_packet::(
+impl RawReadConnection {
+ pub async fn read(&mut self) -> Result, Box> {
+ read_raw_packet::<_>(
&mut self.read_stream,
&mut self.buffer,
self.compression_threshold,
@@ -141,10 +149,8 @@ where
.await
}
- /// Try to read a packet from the stream, or return Ok(None) if there's no
- /// packet.
- pub fn try_read(&mut self) -> Result