aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock210
-rwxr-xr-xCargo.toml1
-rw-r--r--azalea-client/Cargo.toml1
-rw-r--r--azalea-client/src/client.rs127
-rw-r--r--azalea-client/src/interact.rs200
-rw-r--r--azalea-client/src/inventory.rs721
-rw-r--r--azalea-client/src/lib.rs3
-rw-r--r--azalea-client/src/local_player.rs39
-rw-r--r--azalea-client/src/movement.rs49
-rw-r--r--azalea-client/src/packet_handling.rs171
-rwxr-xr-xazalea-client/src/player.rs7
-rwxr-xr-x[-rw-r--r--]azalea-core/Cargo.toml4
-rwxr-xr-xazalea-core/src/aabb.rs87
-rwxr-xr-xazalea-core/src/block_hit_result.rs21
-rwxr-xr-xazalea-core/src/direction.rs52
-rw-r--r--azalea-core/src/game_type.rs73
-rwxr-xr-xazalea-core/src/lib.rs9
-rwxr-xr-xazalea-core/src/particle/mod.rs5
-rwxr-xr-xazalea-core/src/position.rs19
-rw-r--r--azalea-inventory/Cargo.toml12
-rw-r--r--azalea-inventory/README.md2
-rw-r--r--azalea-inventory/azalea-inventory-macros/Cargo.toml14
-rw-r--r--azalea-inventory/azalea-inventory-macros/src/lib.rs45
-rw-r--r--azalea-inventory/azalea-inventory-macros/src/location_enum.rs59
-rw-r--r--azalea-inventory/azalea-inventory-macros/src/menu_enum.rs70
-rw-r--r--azalea-inventory/azalea-inventory-macros/src/menu_impl.rs448
-rw-r--r--azalea-inventory/azalea-inventory-macros/src/parse_macro.rs69
-rw-r--r--azalea-inventory/azalea-inventory-macros/src/utils.rs54
-rw-r--r--azalea-inventory/src/item/mod.rs21
-rw-r--r--azalea-inventory/src/lib.rs172
-rw-r--r--azalea-inventory/src/operations.rs698
-rw-r--r--azalea-inventory/src/slot.rs146
-rwxr-xr-xazalea-nbt/benches/compare.rs28
-rw-r--r--azalea-physics/Cargo.toml1
-rw-r--r--azalea-physics/src/clip.rs232
-rwxr-xr-xazalea-physics/src/collision/discrete_voxel_shape.rs1
-rw-r--r--azalea-physics/src/collision/mod.rs23
-rwxr-xr-xazalea-physics/src/collision/shape.rs62
-rw-r--r--azalea-physics/src/lib.rs49
-rw-r--r--azalea-protocol/Cargo.toml1
-rwxr-xr-xazalea-protocol/src/packets/game/clientbound_container_set_content_packet.rs8
-rwxr-xr-xazalea-protocol/src/packets/game/clientbound_container_set_data_packet.rs2
-rwxr-xr-xazalea-protocol/src/packets/game/clientbound_container_set_slot_packet.rs6
-rwxr-xr-xazalea-protocol/src/packets/game/clientbound_login_packet.rs4
-rwxr-xr-xazalea-protocol/src/packets/game/clientbound_merchant_offers_packet.rs8
-rwxr-xr-xazalea-protocol/src/packets/game/clientbound_open_screen_packet.rs2
-rw-r--r--azalea-protocol/src/packets/game/clientbound_player_info_update_packet.rs6
-rwxr-xr-xazalea-protocol/src/packets/game/clientbound_respawn_packet.rs4
-rwxr-xr-xazalea-protocol/src/packets/game/clientbound_set_equipment_packet.rs6
-rwxr-xr-xazalea-protocol/src/packets/game/clientbound_update_advancements_packet.rs7
-rwxr-xr-xazalea-protocol/src/packets/game/clientbound_update_recipes_packet.rs19
-rwxr-xr-xazalea-protocol/src/packets/game/serverbound_container_click_packet.rs19
-rwxr-xr-xazalea-protocol/src/packets/game/serverbound_set_creative_mode_slot_packet.rs4
-rwxr-xr-xazalea-protocol/src/packets/game/serverbound_use_item_on_packet.rs14
-rwxr-xr-xazalea-registry/src/lib.rs1235
-rw-r--r--azalea-world/Cargo.toml4
-rwxr-xr-xazalea-world/README.md2
-rw-r--r--azalea-world/src/container.rs4
-rwxr-xr-xazalea-world/src/entity/data.rs5
-rw-r--r--azalea-world/src/entity/info.rs18
-rw-r--r--azalea-world/src/entity/metadata.rs53
-rw-r--r--azalea-world/src/entity/mod.rs108
-rw-r--r--azalea-world/src/world.rs22
-rw-r--r--azalea/Cargo.toml1
-rw-r--r--azalea/examples/steal.rs76
-rw-r--r--azalea/examples/testbot.rs81
-rw-r--r--azalea/examples/todo/craft_dig_straight_down.rs8
-rw-r--r--azalea/src/bot.rs18
-rw-r--r--azalea/src/container.rs140
-rw-r--r--azalea/src/lib.rs5
-rw-r--r--azalea/src/pathfinder/mod.rs4
-rw-r--r--azalea/src/prelude.rs5
-rw-r--r--azalea/src/swarm/mod.rs6
-rwxr-xr-xcodegen/genregistries.py2
-rw-r--r--codegen/lib/code/entity.py7
-rw-r--r--codegen/lib/code/inventory.py108
-rwxr-xr-xcodegen/lib/code/registry.py6
-rwxr-xr-xcodegen/migrate.py2
78 files changed, 5633 insertions, 402 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 42fd5161..713c4bf7 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -41,9 +41,9 @@ dependencies = [
[[package]]
name = "aho-corasick"
-version = "0.7.20"
+version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac"
+checksum = "67fc08ce920c31afb70f013dcce1bfc3a3195de6a228474e45e1f145b36f8d04"
dependencies = [
"memchr",
]
@@ -62,9 +62,9 @@ checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
[[package]]
name = "anyhow"
-version = "1.0.70"
+version = "1.0.71"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7de8ce5e0f9f8d88245311066a578d72b7af3e7088f32783804676302df237e4"
+checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8"
[[package]]
name = "async-channel"
@@ -169,6 +169,7 @@ dependencies = [
"azalea-chat",
"azalea-client",
"azalea-core",
+ "azalea-inventory",
"azalea-physics",
"azalea-protocol",
"azalea-registry",
@@ -277,6 +278,7 @@ dependencies = [
"azalea-chat",
"azalea-core",
"azalea-crypto",
+ "azalea-inventory",
"azalea-physics",
"azalea-protocol",
"azalea-registry",
@@ -303,8 +305,12 @@ name = "azalea-core"
version = "0.6.0"
dependencies = [
"azalea-buf",
+ "azalea-chat",
+ "azalea-inventory",
"azalea-nbt",
+ "azalea-registry",
"bevy_ecs",
+ "num-traits",
"serde",
"uuid",
]
@@ -325,6 +331,25 @@ dependencies = [
]
[[package]]
+name = "azalea-inventory"
+version = "0.1.0"
+dependencies = [
+ "azalea-buf",
+ "azalea-inventory-macros",
+ "azalea-nbt",
+ "azalea-registry",
+]
+
+[[package]]
+name = "azalea-inventory-macros"
+version = "0.1.0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
name = "azalea-language"
version = "0.6.0"
dependencies = [
@@ -357,6 +382,7 @@ version = "0.6.0"
dependencies = [
"azalea-block",
"azalea-core",
+ "azalea-inventory",
"azalea-registry",
"azalea-world",
"bevy_app",
@@ -381,6 +407,7 @@ dependencies = [
"azalea-chat",
"azalea-core",
"azalea-crypto",
+ "azalea-inventory",
"azalea-nbt",
"azalea-protocol-macros",
"azalea-registry",
@@ -438,7 +465,9 @@ dependencies = [
"azalea-block",
"azalea-buf",
"azalea-chat",
+ "azalea-client",
"azalea-core",
+ "azalea-inventory",
"azalea-nbt",
"azalea-registry",
"bevy_app",
@@ -463,7 +492,7 @@ dependencies = [
"cc",
"cfg-if",
"libc",
- "miniz_oxide",
+ "miniz_oxide 0.6.2",
"object",
"rustc-demangle",
]
@@ -698,9 +727,9 @@ dependencies = [
[[package]]
name = "bumpalo"
-version = "3.12.0"
+version = "3.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535"
+checksum = "9b1ce199063694f33ffb7dd4e0ee620741495c32833cde5aa08f02a0bf96f0c8"
[[package]]
name = "bytemuck"
@@ -811,9 +840,9 @@ dependencies = [
[[package]]
name = "clap"
-version = "3.2.23"
+version = "3.2.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "71655c45cb9845d3270c9d6df84ebe72b4dad3c2ba3f7023ad47c144e4e473a5"
+checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123"
dependencies = [
"bitflags",
"clap_lex",
@@ -871,9 +900,9 @@ checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
[[package]]
name = "cpufeatures"
-version = "0.2.6"
+version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "280a9f2d8b3a38871a3c8a46fb80db65e5e5ed97da80c4d08bf27fb63e35e181"
+checksum = "3e4c1eaa2012c47becbbad2ab175484c2a84d1185b566fb2cc5b8707343dfe58"
dependencies = [
"libc",
]
@@ -1068,9 +1097,9 @@ checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
[[package]]
name = "fastnbt"
-version = "2.4.3"
+version = "2.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f1aab2b0109236f6c89cc81b9e2ef4aced6d585aabe96ac860ee5e9a102eb198"
+checksum = "3369bd70629bccfda7e344883c9ae3ab7f3b10a357bcf8b0f69caa7256bcf188"
dependencies = [
"byteorder",
"cesu8",
@@ -1095,12 +1124,12 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flate2"
-version = "1.0.25"
+version = "1.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841"
+checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743"
dependencies = [
"crc32fast",
- "miniz_oxide",
+ "miniz_oxide 0.7.1",
]
[[package]]
@@ -1512,9 +1541,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libc"
-version = "0.2.141"
+version = "0.2.142"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3304a64d199bb964be99741b7a14d26972741915b3649639149b2479bb46f4b5"
+checksum = "6a987beff54b60ffa6d51982e1aa1146bc42f19bd26be28b0586f252fccf5317"
[[package]]
name = "libm"
@@ -1602,6 +1631,15 @@ dependencies = [
]
[[package]]
+name = "miniz_oxide"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7"
+dependencies = [
+ "adler",
+]
+
+[[package]]
name = "mio"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1610,7 +1648,7 @@ dependencies = [
"libc",
"log",
"wasi",
- "windows-sys",
+ "windows-sys 0.45.0",
]
[[package]]
@@ -1787,7 +1825,7 @@ dependencies = [
"redox_syscall",
"smallvec",
"thread-id",
- "windows-sys",
+ "windows-sys 0.45.0",
]
[[package]]
@@ -1943,13 +1981,13 @@ dependencies = [
[[package]]
name = "regex"
-version = "1.7.3"
+version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8b1f693b24f6ac912f4893ef08244d70b6067480d2f1a46e950c9691e6749d1d"
+checksum = "af83e617f331cc6ae2da5443c602dfa5af81e517212d9d611a5b3ba1777b5370"
dependencies = [
"aho-corasick",
"memchr",
- "regex-syntax",
+ "regex-syntax 0.7.1",
]
[[package]]
@@ -1958,7 +1996,7 @@ version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
dependencies = [
- "regex-syntax",
+ "regex-syntax 0.6.29",
]
[[package]]
@@ -1968,10 +2006,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
[[package]]
+name = "regex-syntax"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a5996294f19bd3aae0453a862ad728f60e6600695733dd5df01da90c54363a3c"
+
+[[package]]
name = "reqwest"
-version = "0.11.16"
+version = "0.11.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "27b71749df584b7f4cac2c426c127a7c785a5106cc98f7a8feb044115f0fa254"
+checksum = "13293b639a097af28fc8a90f22add145a9c954e49d77da06263d58cf44d5fb91"
dependencies = [
"base64",
"bytes",
@@ -2355,9 +2399,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
-version = "1.27.0"
+version = "1.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d0de47a4eecbe11f498978a9b29d792f0d2692d1dd003650c24c76510e3bc001"
+checksum = "c3c786bf8134e5a3a166db9b29ab8f48134739014a3eca7bc6bfa95d673b136f"
dependencies = [
"autocfg",
"bytes",
@@ -2369,14 +2413,14 @@ dependencies = [
"signal-hook-registry",
"socket2",
"tokio-macros",
- "windows-sys",
+ "windows-sys 0.48.0",
]
[[package]]
name = "tokio-macros"
-version = "2.0.0"
+version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "61a573bdc87985e9d6ddeed1b3d864e8a302c847e40d647746df2f1de209d1ce"
+checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e"
dependencies = [
"proc-macro2",
"quote",
@@ -2396,9 +2440,9 @@ dependencies = [
[[package]]
name = "tokio-util"
-version = "0.7.7"
+version = "0.7.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5427d89453009325de0d8f342c9490009f76e999cb7672d77e46267448f7e6b2"
+checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d"
dependencies = [
"bytes",
"futures-core",
@@ -2445,13 +2489,13 @@ dependencies = [
[[package]]
name = "tracing-attributes"
-version = "0.1.23"
+version = "0.1.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a"
+checksum = "0f57e3ca2a01450b1a921183a9c9cbfda207fd822cef4ccb00a65402cbba7a74"
dependencies = [
"proc-macro2",
"quote",
- "syn 1.0.109",
+ "syn 2.0.15",
]
[[package]]
@@ -2477,9 +2521,9 @@ dependencies = [
[[package]]
name = "tracing-subscriber"
-version = "0.3.16"
+version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a6176eae26dd70d0c919749377897b54a9276bd7061339665dd68777926b5a70"
+checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77"
dependencies = [
"matchers",
"nu-ansi-term",
@@ -2599,9 +2643,9 @@ dependencies = [
[[package]]
name = "uuid"
-version = "1.3.1"
+version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5b55a3fef2a1e3b3a00ce878640918820d3c51081576ac657d23af9fc7928fdb"
+checksum = "4dad5567ad0cf5b760e5665964bec1b47dfd077ba8a2544b513f3556d3d239a2"
dependencies = [
"getrandom",
"serde",
@@ -2794,7 +2838,16 @@ version = "0.45.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
dependencies = [
- "windows-targets",
+ "windows-targets 0.42.2",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
+dependencies = [
+ "windows-targets 0.48.0",
]
[[package]]
@@ -2803,13 +2856,28 @@ version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071"
dependencies = [
- "windows_aarch64_gnullvm",
- "windows_aarch64_msvc",
- "windows_i686_gnu",
- "windows_i686_msvc",
- "windows_x86_64_gnu",
- "windows_x86_64_gnullvm",
- "windows_x86_64_msvc",
+ "windows_aarch64_gnullvm 0.42.2",
+ "windows_aarch64_msvc 0.42.2",
+ "windows_i686_gnu 0.42.2",
+ "windows_i686_msvc 0.42.2",
+ "windows_x86_64_gnu 0.42.2",
+ "windows_x86_64_gnullvm 0.42.2",
+ "windows_x86_64_msvc 0.42.2",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5"
+dependencies = [
+ "windows_aarch64_gnullvm 0.48.0",
+ "windows_aarch64_msvc 0.48.0",
+ "windows_i686_gnu 0.48.0",
+ "windows_i686_msvc 0.48.0",
+ "windows_x86_64_gnu 0.48.0",
+ "windows_x86_64_gnullvm 0.48.0",
+ "windows_x86_64_msvc 0.48.0",
]
[[package]]
@@ -2819,46 +2887,88 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc"
+
+[[package]]
name = "windows_aarch64_msvc"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
[[package]]
+name = "windows_aarch64_msvc"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3"
+
+[[package]]
name = "windows_i686_gnu"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
[[package]]
+name = "windows_i686_gnu"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241"
+
+[[package]]
name = "windows_i686_msvc"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
[[package]]
+name = "windows_i686_msvc"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00"
+
+[[package]]
name = "windows_x86_64_gnu"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
[[package]]
+name = "windows_x86_64_gnu"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1"
+
+[[package]]
name = "windows_x86_64_gnullvm"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953"
+
+[[package]]
name = "windows_x86_64_msvc"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
[[package]]
+name = "windows_x86_64_msvc"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
+
+[[package]]
name = "winnow"
-version = "0.4.1"
+version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ae8970b36c66498d8ff1d66685dc86b91b29db0c7739899012f63a63814b4b28"
+checksum = "5617da7e1f97bf363947d767b91aaf3c2bbc19db7fda9c65af1278713d58e0a2"
dependencies = [
"memchr",
]
diff --git a/Cargo.toml b/Cargo.toml
index b7439bd1..fdd67549 100755
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -15,6 +15,7 @@ members = [
"azalea-buf",
"azalea-physics",
"azalea-registry",
+ "azalea-inventory",
]
[profile.release]
diff --git a/azalea-client/Cargo.toml b/azalea-client/Cargo.toml
index 6bc07def..2a80467a 100644
--- a/azalea-client/Cargo.toml
+++ b/azalea-client/Cargo.toml
@@ -25,6 +25,7 @@ bevy_ecs = "0.10.0"
bevy_log = "0.10.0"
bevy_tasks = "0.10.0"
bevy_time = "0.10.0"
+azalea-inventory = { path = "../azalea-inventory", version = "0.1.0" }
derive_more = { version = "0.99.17", features = ["deref", "deref_mut"] }
futures = "0.3.25"
log = "0.4.17"
diff --git a/azalea-client/src/client.rs b/azalea-client/src/client.rs
index 47cc7235..7a4285e6 100644
--- a/azalea-client/src/client.rs
+++ b/azalea-client/src/client.rs
@@ -2,11 +2,13 @@ use crate::{
chat::ChatPlugin,
disconnect::{DisconnectEvent, DisconnectPlugin},
events::{Event, EventPlugin, LocalPlayerEvents},
+ interact::{CurrentSequenceNumber, InteractPlugin},
+ inventory::{InventoryComponent, InventoryPlugin},
local_player::{
death_event, handle_send_packet_event, update_in_loaded_chunk, GameProfileComponent,
LocalPlayer, PhysicsState, SendPacketEvent,
},
- movement::PlayerMovePlugin,
+ movement::{LastSentLookDirection, PlayerMovePlugin},
packet_handling::{self, PacketHandlerPlugin, PacketReceiver},
player::retroactively_add_game_profile_component,
task_pool::TaskPoolPlugin,
@@ -15,11 +17,13 @@ use crate::{
use azalea_auth::{game_profile::GameProfile, sessionserver::ClientSessionServerError};
use azalea_chat::FormattedText;
+use azalea_core::Vec3;
use azalea_physics::{PhysicsPlugin, PhysicsSet};
use azalea_protocol::{
connect::{Connection, ConnectionError},
packets::{
game::{
+ clientbound_player_abilities_packet::ClientboundPlayerAbilitiesPacket,
serverbound_client_information_packet::ServerboundClientInformationPacket,
ClientboundGamePacket, ServerboundGamePacket,
},
@@ -37,16 +41,17 @@ use azalea_protocol::{
resolver, ServerAddress,
};
use azalea_world::{
- entity::{EntityPlugin, EntityUpdateSet, Local, WorldName},
+ entity::{EntityPlugin, EntityUpdateSet, Local, Position, WorldName},
Instance, InstanceContainer, PartialInstance,
};
-use bevy_app::{App, CoreSchedule, Plugin, PluginGroup, PluginGroupBuilder};
+use bevy_app::{App, CoreSchedule, IntoSystemAppConfig, Plugin, PluginGroup, PluginGroupBuilder};
use bevy_ecs::{
bundle::Bundle,
component::Component,
entity::Entity,
schedule::IntoSystemConfig,
schedule::{LogLevel, ScheduleBuildSettings, ScheduleLabel},
+ system::{ResMut, Resource},
world::World,
};
use bevy_log::LogPlugin;
@@ -56,7 +61,10 @@ use log::{debug, error};
use parking_lot::{Mutex, RwLock};
use std::{collections::HashMap, fmt::Debug, io, net::SocketAddr, sync::Arc, time::Duration};
use thiserror::Error;
-use tokio::{sync::mpsc, time};
+use tokio::{
+ sync::{broadcast, mpsc},
+ time,
+};
use uuid::Uuid;
/// `Client` has the things that a user interacting with the library will want.
@@ -93,11 +101,50 @@ pub struct Client {
}
/// A component that contains some of the "settings" for this client that are
-/// sent to the server, such as render distance.
+/// 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<ClientboundPlayerAbilitiesPacket> 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,
+ }
+ }
+}
+
/// A component that contains a map of player UUIDs to their information in the
-/// tab list
+/// tab list.
+///
+/// ```
+/// # use azalea_client::TabList;
+/// # fn example(client: &azalea_client::Client) {
+/// let tab_list = client.component::<TabList>();
+/// println!("Online players:");
+/// for (uuid, player_info) in tab_list.iter() {
+/// println!("- {} ({}ms)", player_info.profile.name, player_info.latency);
+/// }
+/// # }
#[derive(Component, Clone, Debug, Deref, DerefMut, Default)]
pub struct TabList(HashMap<Uuid, PlayerInfo>);
@@ -246,8 +293,12 @@ impl Client {
game_profile: GameProfileComponent(game_profile),
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(),
_local: Local,
});
@@ -421,6 +472,11 @@ impl Client {
self.query::<&T>(&mut self.ecs.lock()).clone()
}
+ /// Get a component from this client, or `None` if it doesn't exist.
+ pub fn get_component<T: Component + Clone>(&self) -> Option<T> {
+ self.query::<Option<&T>>(&mut self.ecs.lock()).cloned()
+ }
+
/// Get a reference to our (potentially shared) world.
///
/// This gets the [`Instance`] from our world container. If it's a normal
@@ -430,8 +486,8 @@ impl Client {
pub fn world(&self) -> Arc<RwLock<Instance>> {
let world_name = self.component::<WorldName>();
let ecs = self.ecs.lock();
- let world_container = ecs.resource::<InstanceContainer>();
- world_container.get(&world_name).unwrap()
+ let instance_container = ecs.resource::<InstanceContainer>();
+ instance_container.get(&world_name).unwrap()
}
/// Returns whether we have a received the login packet yet.
@@ -478,6 +534,15 @@ impl Client {
}
}
+impl Client {
+ /// Get the position of this client.
+ ///
+ /// This is a shortcut for `Vec3::from(&bot.component::<Position>())`.
+ pub fn position(&self) -> Vec3 {
+ Vec3::from(&self.component::<Position>())
+ }
+}
+
/// 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 [`Local`].
#[derive(Bundle)]
@@ -487,8 +552,12 @@ pub struct JoinedClientBundle {
pub game_profile: GameProfileComponent,
pub physics_state: PhysicsState,
pub local_player_events: LocalPlayerEvents,
+ pub inventory: InventoryComponent,
pub client_information: ClientInformation,
pub tab_list: TabList,
+ pub current_sequence_number: CurrentSequenceNumber,
+ pub last_sent_direction: LastSentLookDirection,
+ pub abilities: PlayerAbilities,
pub _local: Local,
}
@@ -498,11 +567,7 @@ impl Plugin for AzaleaPlugin {
// Minecraft ticks happen every 50ms
app.insert_resource(FixedTime::new(Duration::from_millis(50)));
- app.add_system(
- update_in_loaded_chunk
- .after(PhysicsSet)
- .after(handle_send_packet_event),
- );
+ app.add_system(update_in_loaded_chunk.after(PhysicsSet));
// fire the Death event when the player dies.
app.add_system(death_event);
@@ -599,6 +664,39 @@ pub async fn tick_run_schedule_loop(run_schedule_sender: mpsc::UnboundedSender<(
}
}
+/// A resource that contains a [`broadcast::Sender`] that will be sent every
+/// Minecraft tick.
+///
+/// This is useful for running code every schedule from async user code.
+///
+/// ```
+/// use azalea_client::TickBroadcast;
+/// # async fn example(client: azalea_client::Client) {
+/// let mut receiver = {
+/// let ecs = client.ecs.lock();
+/// let tick_broadcast = ecs.resource::<TickBroadcast>();
+/// tick_broadcast.subscribe()
+/// };
+/// while receiver.recv().await.is_ok() {
+/// // do something
+/// }
+/// # }
+/// ```
+#[derive(Resource, Deref)]
+pub struct TickBroadcast(broadcast::Sender<()>);
+
+fn send_tick_broadcast(tick_broadcast: ResMut<TickBroadcast>) {
+ let _ = tick_broadcast.0.send(());
+}
+/// A plugin that makes the [`RanScheduleBroadcast`] resource available.
+pub struct TickBroadcastPlugin;
+impl Plugin for TickBroadcastPlugin {
+ fn build(&self, app: &mut App) {
+ app.insert_resource(TickBroadcast(broadcast::channel(1).0))
+ .add_system(send_tick_broadcast.in_schedule(CoreSchedule::FixedUpdate));
+ }
+}
+
/// This plugin group will add all the default plugins necessary for Azalea to
/// work.
pub struct DefaultPlugins;
@@ -614,8 +712,11 @@ impl PluginGroup for DefaultPlugins {
.add(PhysicsPlugin)
.add(EventPlugin)
.add(TaskPoolPlugin::default())
+ .add(InventoryPlugin)
.add(ChatPlugin)
.add(DisconnectPlugin)
.add(PlayerMovePlugin)
+ .add(InteractPlugin)
+ .add(TickBroadcastPlugin)
}
}
diff --git a/azalea-client/src/interact.rs b/azalea-client/src/interact.rs
new file mode 100644
index 00000000..ec5ed87b
--- /dev/null
+++ b/azalea-client/src/interact.rs
@@ -0,0 +1,200 @@
+use azalea_core::{BlockHitResult, BlockPos, Direction, GameMode, Vec3};
+use azalea_physics::clip::{BlockShapeType, ClipContext, FluidPickType};
+use azalea_protocol::packets::game::{
+ serverbound_interact_packet::InteractionHand,
+ serverbound_use_item_on_packet::{BlockHit, ServerboundUseItemOnPacket},
+};
+use azalea_world::{
+ entity::{clamp_look_direction, view_vector, EyeHeight, LookDirection, Position, WorldName},
+ InstanceContainer,
+};
+use bevy_app::{App, Plugin};
+use bevy_ecs::{
+ component::Component,
+ entity::Entity,
+ event::EventReader,
+ schedule::{IntoSystemConfig, IntoSystemConfigs},
+ system::{Commands, Query, Res},
+};
+use derive_more::{Deref, DerefMut};
+use log::warn;
+
+use crate::{
+ local_player::{handle_send_packet_event, LocalGameMode},
+ Client, LocalPlayer,
+};
+
+/// A plugin that allows clients to interact with blocks in the world.
+pub struct InteractPlugin;
+impl Plugin for InteractPlugin {
+ fn build(&self, app: &mut App) {
+ app.add_event::<BlockInteractEvent>().add_systems(
+ (
+ update_hit_result_component.after(clamp_look_direction),
+ handle_block_interact_event,
+ )
+ .before(handle_send_packet_event)
+ .chain(),
+ );
+ }
+}
+
+impl Client {
+ /// Right click a block. The behavior of this depends on the target block,
+ /// and it'll either place the block you're holding in your hand or use the
+ /// block you clicked (like toggling a lever).
+ ///
+ /// Note that this may trigger anticheats as it doesn't take into account
+ /// whether you're actually looking at the block.
+ pub fn block_interact(&mut self, position: BlockPos) {
+ self.ecs.lock().send_event(BlockInteractEvent {
+ entity: self.entity,
+ position,
+ });
+ }
+}
+
+/// Right click a block. The behavior of this depends on the target block,
+/// and it'll either place the block you're holding in your hand or use the
+/// block you clicked (like toggling a lever).
+pub struct BlockInteractEvent {
+ /// The local player entity that's opening the container.
+ pub entity: Entity,
+ /// The coordinates of the container.
+ pub position: BlockPos,
+}
+
+/// A component that contains the number of changes this client has made to
+/// blocks.
+#[derive(Component, Copy, Clone, Debug, Default, Deref, DerefMut)]
+pub struct CurrentSequenceNumber(u32);
+
+/// A component that contains the block that the player is currently looking at.
+#[derive(Component, Clone, Debug, Deref, DerefMut)]
+pub struct HitResultComponent(BlockHitResult);
+
+fn handle_block_interact_event(
+ mut events: EventReader<BlockInteractEvent>,
+ mut query: Query<(
+ &LocalPlayer,
+ &mut CurrentSequenceNumber,
+ &HitResultComponent,
+ )>,
+) {
+ 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");
+ continue;
+ };
+
+ // TODO: check to make sure we're within the world border
+
+ **sequence_number += 1;
+
+ // minecraft also does the interaction client-side (so it looks like clicking a
+ // button is instant) but we don't really need that
+
+ // the block_hit data will depend on whether we're looking at the block and
+ // whether we can reach it
+
+ let block_hit = if hit_result.block_pos == event.position {
+ // we're looking at the block :)
+ BlockHit {
+ block_pos: hit_result.block_pos,
+ direction: hit_result.direction,
+ location: hit_result.location,
+ inside: hit_result.inside,
+ }
+ } else {
+ // we're not looking at the block, so make up some numbers
+ BlockHit {
+ block_pos: event.position,
+ direction: Direction::Up,
+ location: event.position.center(),
+ inside: false,
+ }
+ };
+
+ local_player.write_packet(
+ ServerboundUseItemOnPacket {
+ hand: InteractionHand::MainHand,
+ block_hit,
+ sequence: sequence_number.0,
+ }
+ .get(),
+ )
+ }
+}
+
+#[allow(clippy::type_complexity)]
+fn update_hit_result_component(
+ mut commands: Commands,
+ mut query: Query<(
+ Entity,
+ Option<&mut HitResultComponent>,
+ &LocalGameMode,
+ &Position,
+ &EyeHeight,
+ &LookDirection,
+ &WorldName,
+ )>,
+ instance_container: Res<InstanceContainer>,
+) {
+ for (entity, hit_result_ref, game_mode, position, eye_height, look_direction, world_name) in
+ &mut query
+ {
+ let pick_range = if game_mode.current == GameMode::Creative {
+ 6.
+ } else {
+ 4.5
+ };
+ let eye_position = Vec3 {
+ x: position.x,
+ y: position.y + **eye_height as f64,
+ z: position.z,
+ };
+ let hit_result = pick(
+ look_direction,
+ &eye_position,
+ world_name,
+ &instance_container,
+ pick_range,
+ );
+ if let Some(mut hit_result_ref) = hit_result_ref {
+ **hit_result_ref = hit_result;
+ } else {
+ commands
+ .entity(entity)
+ .insert(HitResultComponent(hit_result));
+ }
+ }
+}
+
+/// Get the block that a player would be looking at if their eyes were at the
+/// given direction and position.
+///
+/// If you need to get the block the player is looking at right now, use
+/// [`HitResultComponent`].
+pub fn pick(
+ look_direction: &LookDirection,
+ eye_position: &Vec3,
+ world_name: &WorldName,
+ instance_container: &InstanceContainer,
+ pick_range: f64,
+) -> BlockHitResult {
+ let view_vector = view_vector(look_direction);
+ let end_position = eye_position + &(view_vector * pick_range);
+ let instance_lock = instance_container
+ .get(world_name)
+ .expect("entities must always be in a valid world");
+ let instance = instance_lock.read();
+ azalea_physics::clip::clip(
+ &instance.chunks,
+ ClipContext {
+ from: *eye_position,
+ to: end_position,
+ block_shape_type: BlockShapeType::Outline,
+ fluid_pick_type: FluidPickType::None,
+ },
+ )
+}
diff --git a/azalea-client/src/inventory.rs b/azalea-client/src/inventory.rs
new file mode 100644
index 00000000..d6f909a7
--- /dev/null
+++ b/azalea-client/src/inventory.rs
@@ -0,0 +1,721 @@
+use std::collections::{HashMap, HashSet};
+
+use azalea_chat::FormattedText;
+pub use azalea_inventory::*;
+use azalea_inventory::{
+ item::MaxStackSizeExt,
+ operations::{
+ ClickOperation, CloneClick, PickupAllClick, PickupClick, QuickCraftKind, QuickCraftStatus,
+ QuickCraftStatusKind, QuickMoveClick, ThrowClick,
+ },
+};
+use azalea_protocol::packets::game::{
+ serverbound_container_click_packet::ServerboundContainerClickPacket,
+ serverbound_container_close_packet::ServerboundContainerClosePacket,
+};
+use azalea_registry::MenuKind;
+use bevy_app::{App, Plugin};
+use bevy_ecs::{
+ component::Component,
+ entity::Entity,
+ event::EventReader,
+ prelude::EventWriter,
+ schedule::{IntoSystemConfig, IntoSystemConfigs},
+ system::Query,
+};
+use log::warn;
+
+use crate::{client::PlayerAbilities, local_player::handle_send_packet_event, Client, LocalPlayer};
+
+pub struct InventoryPlugin;
+impl Plugin for InventoryPlugin {
+ fn build(&self, app: &mut App) {
+ app.add_event::<ClientSideCloseContainerEvent>()
+ .add_event::<MenuOpenedEvent>()
+ .add_event::<CloseContainerEvent>()
+ .add_event::<ContainerClickEvent>()
+ .add_event::<SetContainerContentEvent>()
+ .add_systems(
+ (
+ handle_menu_opened_event,
+ handle_set_container_content_event,
+ handle_container_click_event,
+ handle_container_close_event.before(handle_send_packet_event),
+ handle_client_side_close_container_event,
+ )
+ .chain(),
+ );
+ }
+}
+
+impl Client {
+ /// Return the menu that is currently open. If no menu is open, this will
+ /// have the player's inventory.
+ pub fn menu(&self) -> Menu {
+ let mut ecs = self.ecs.lock();
+ let inventory = self.query::<&InventoryComponent>(&mut ecs);
+ inventory.menu().clone()
+ }
+}
+
+/// A component present on all local players that have an inventory.
+#[derive(Component, Debug)]
+pub struct InventoryComponent {
+ /// A component that contains the player's inventory menu. This is
+ /// guaranteed to be a `Menu::Player`.
+ ///
+ /// We keep it as a [`Menu`] since `Menu` has some useful functions that
+ /// bare [`azalea_inventory::Player`] doesn't have.
+ pub inventory_menu: azalea_inventory::Menu,
+
+ /// The ID of the container that's currently open. Its value is not
+ /// guaranteed to be anything specific, and may change every time you open a
+ /// container (unless it's 0, in which case it means that no container is
+ /// open).
+ pub id: u8,
+ /// The current container menu that the player has open. If no container is
+ /// open, this will be `None`.
+ pub container_menu: Option<azalea_inventory::Menu>,
+ /// The item that is currently held by the cursor. `Slot::Empty` if nothing
+ /// is currently being held.
+ pub carried: ItemSlot,
+ /// An identifier used by the server to track client inventory desyncs. This
+ /// is sent on every container click, and it's only ever updated when the
+ /// server sends a new container update.
+ pub state_id: u32,
+
+ pub quick_craft_status: QuickCraftStatusKind,
+ pub quick_craft_kind: QuickCraftKind,
+ /// A set of the indexes of the slots that have been right clicked in
+ /// this "quick craft".
+ pub quick_craft_slots: HashSet<u16>,
+ // minecraft also has these fields, but i don't
+ // think they're necessary?:
+ // private final NonNullList<ItemStack>
+ // remoteSlots;
+ // private final IntList remoteDataSlots;
+ // private ItemStack remoteCarried;
+}
+impl InventoryComponent {
+ /// Returns a reference to the currently active menu. If a container is open
+ /// it'll return [`Self::container_menu`], otherwise
+ /// [`Self::inventory_menu`].
+ ///
+ /// Use [`Self::menu_mut`] if you need a mutable reference.
+ pub fn menu(&self) -> &azalea_inventory::Menu {
+ if let Some(menu) = &self.container_menu {
+ menu
+ } else {
+ &self.inventory_menu
+ }
+ }
+
+ /// Returns a mutable reference to the currently active menu. If a container
+ /// is open it'll return [`Self::container_menu`], otherwise
+ /// [`Self::inventory_menu`].
+ ///
+ /// Use [`Self::menu`] if you don't need a mutable reference.
+ pub fn menu_mut(&mut self) -> &mut azalea_inventory::Menu {
+ if let Some(menu) = &mut self.container_menu {
+ menu
+ } else {
+ &mut self.inventory_menu
+ }
+ }
+
+ /// Modify the inventory as if the given operation was performed on it.
+ pub fn simulate_click(
+ &mut self,
+ operation: &ClickOperation,
+ player_abilities: &PlayerAbilities,
+ ) {
+ if let ClickOperation::QuickCraft(quick_craft) = operation {
+ let last_quick_craft_status_tmp = self.quick_craft_status.clone();
+ self.quick_craft_status = last_quick_craft_status_tmp.clone();
+ let last_quick_craft_status = last_quick_craft_status_tmp;
+
+ // no carried item, reset
+ if self.carried.is_empty() {
+ return self.reset_quick_craft();
+ }
+ // if we were starting or ending, or now we aren't ending and the status
+ // changed, reset
+ if (last_quick_craft_status == QuickCraftStatusKind::Start
+ || last_quick_craft_status == QuickCraftStatusKind::End
+ || self.quick_craft_status != QuickCraftStatusKind::End)
+ && (self.quick_craft_status != last_quick_craft_status)
+ {
+ return self.reset_quick_craft();
+ }
+ if self.quick_craft_status == QuickCraftStatusKind::Start {
+ self.quick_craft_kind = quick_craft.kind.clone();
+ if self.quick_craft_kind == QuickCraftKind::Middle && player_abilities.instant_break
+ {
+ self.quick_craft_status = QuickCraftStatusKind::Add;
+ self.quick_craft_slots.clear();
+ } else {
+ self.reset_quick_craft();
+ }
+ return;
+ }
+ if let QuickCraftStatus::Add { slot } = quick_craft.status {
+ let slot_item = self.menu().slot(slot as usize);
+ if let Some(slot_item) = slot_item {
+ if let ItemSlot::Present(carried) = &self.carried {
+ // minecraft also checks slot.may_place(carried) and
+ // menu.can_drag_to(slot)
+ // but they always return true so they're not relevant for us
+ if can_item_quick_replace(slot_item, &self.carried, true)
+ && (self.quick_craft_kind == QuickCraftKind::Right
+ || carried.count as usize > self.quick_craft_slots.len())
+ {
+ self.quick_craft_slots.insert(slot);
+ }
+ }
+ }
+ return;
+ }
+ if self.quick_craft_status == QuickCraftStatusKind::End {
+ if !self.quick_craft_slots.is_empty() {
+ if self.quick_craft_slots.len() == 1 {
+ // if we only clicked one slot, then turn this
+ // QuickCraftClick into a PickupClick
+ let slot = *self.quick_craft_slots.iter().next().unwrap();
+ self.reset_quick_craft();
+ self.simulate_click(
+ &match self.quick_craft_kind {
+ QuickCraftKind::Left => {
+ PickupClick::Left { slot: Some(slot) }.into()
+ }
+ QuickCraftKind::Right => {
+ PickupClick::Left { slot: Some(slot) }.into()
+ }
+ QuickCraftKind::Middle => {
+ // idk just do nothing i guess
+ return;
+ }
+ },
+ player_abilities,
+ );
+ return;
+ }
+
+ let ItemSlot::Present(mut carried) = self.carried.clone() else {
+ // this should never happen
+ return self.reset_quick_craft();
+ };
+
+ let mut carried_count = carried.count;
+ let mut quick_craft_slots_iter = self.quick_craft_slots.iter();
+
+ loop {
+ let mut slot: &ItemSlot;
+ let mut slot_index: u16;
+ let mut item_stack: &ItemSlot;
+
+ loop {
+ let Some(&next_slot) = quick_craft_slots_iter.next() else {
+ carried.count = carried_count;
+ self.carried = ItemSlot::Present(carried);
+ return self.reset_quick_craft();
+ };
+
+ slot = self.menu().slot(next_slot as usize).unwrap();
+ slot_index = next_slot;
+ item_stack = &self.carried;
+
+ if slot.is_present()
+ && can_item_quick_replace(slot, item_stack, true)
+ // this always returns true in most cases
+ // && slot.may_place(item_stack)
+ && (
+ self.quick_craft_kind == QuickCraftKind::Middle
+ || item_stack.count() as i32 >= self.quick_craft_slots.len() as i32
+ )
+ {
+ break;
+ }
+ }
+
+ // get the ItemSlotData for the slot
+ let ItemSlot::Present(slot) = slot else {
+ unreachable!("the loop above requires the slot to be present to break")
+ };
+
+ // if self.can_drag_to(slot) {
+ let mut new_carried = carried.clone();
+ let slot_item_count = slot.count;
+ get_quick_craft_slot_count(
+ &self.quick_craft_slots,
+ &self.quick_craft_kind,
+ &mut new_carried,
+ slot_item_count,
+ );
+ let max_stack_size = i8::min(
+ new_carried.kind.max_stack_size(),
+ i8::min(
+ new_carried.kind.max_stack_size(),
+ slot.kind.max_stack_size(),
+ ),
+ );
+ if new_carried.count > max_stack_size {
+ new_carried.count = max_stack_size;
+ }
+
+ carried_count -= new_carried.count - slot_item_count;
+ // we have to inline self.menu_mut() here to avoid the borrow checker
+ // complaining
+ let menu = if let Some(menu) = &mut self.container_menu {
+ menu
+ } else {
+ &mut self.inventory_menu
+ };
+ *menu.slot_mut(slot_index as usize).unwrap() =
+ ItemSlot::Present(new_carried);
+ // }
+ }
+ }
+ } else {
+ return self.reset_quick_craft();
+ }
+ }
+ // the quick craft status should always be in start if we're not in quick craft
+ // mode
+ if self.quick_craft_status != QuickCraftStatusKind::Start {
+ return self.reset_quick_craft();
+ }
+
+ match operation {
+ // left clicking outside inventory
+ ClickOperation::Pickup(PickupClick::Left { slot: None }) => {
+ if self.carried.is_present() {
+ // vanilla has `player.drop`s but they're only used
+ // server-side
+ // they're included as comments here in case you want to adapt this for a server
+ // implementation
+
+ // player.drop(self.carried, true);
+ self.carried = ItemSlot::Empty;
+ }
+ }
+ ClickOperation::Pickup(PickupClick::Right { slot: None }) => {
+ if self.carried.is_present() {
+ let _item = self.carried.split(1);
+ // player.drop(item, true);
+ }
+ }
+ ClickOperation::Pickup(
+ PickupClick::Left { slot: Some(slot) } | PickupClick::Right { slot: Some(slot) },
+ ) => {
+ let Some(slot_item) = self.menu().slot(*slot as usize) else {
+ return;
+ };
+ let carried = &self.carried;
+ // vanilla does a check called tryItemClickBehaviourOverride
+ // here
+ // i don't understand it so i didn't implement it
+ match slot_item {
+ ItemSlot::Empty => if carried.is_present() {},
+ ItemSlot::Present(_) => todo!(),
+ }
+ }
+ ClickOperation::QuickMove(
+ QuickMoveClick::Left { slot } | QuickMoveClick::Right { slot },
+ ) => {
+ // in vanilla it also tests if QuickMove has a slot index of -999
+ // but i don't think that's ever possible so it's not covered here
+ loop {
+ let new_slot_item = self.menu_mut().quick_move_stack(*slot as usize);
+ let slot_item = self.menu().slot(*slot as usize).unwrap();
+ if new_slot_item.is_empty() || slot_item != &new_slot_item {
+ break;
+ }
+ }
+ }
+ ClickOperation::Swap(s) => {
+ let source_slot_index = s.source_slot as usize;
+ let target_slot_index = s.target_slot as usize;
+
+ let Some(source_slot) = self.menu().slot(source_slot_index) else {
+ return;
+ };
+ let Some(target_slot) = self.menu().slot(target_slot_index) else {
+ return;
+ };
+ if source_slot.is_empty() && target_slot.is_empty() {
+ return;
+ }
+
+ if target_slot.is_empty() {
+ if self.menu().may_pickup(source_slot_index) {
+ let source_slot = source_slot.clone();
+ let target_slot = self.menu_mut().slot_mut(target_slot_index).unwrap();
+ *target_slot = source_slot;
+ }
+ } else if source_slot.is_empty() {
+ let ItemSlot::Present(target_item) = target_slot else {
+ unreachable!("target slot is not empty but is not present");
+ };
+ if self.menu().may_place(source_slot_index, target_item) {
+ // get the target_item but mutable
+ let source_max_stack_size = self.menu().max_stack_size(source_slot_index);
+
+ let target_slot = self.menu_mut().slot_mut(target_slot_index).unwrap();
+ let new_source_slot = target_slot.split(source_max_stack_size);
+ *self.menu_mut().slot_mut(source_slot_index).unwrap() = new_source_slot;
+ }
+ } else if self.menu().may_pickup(source_slot_index) {
+ let ItemSlot::Present(target_item) = target_slot else {
+ unreachable!("target slot is not empty but is not present");
+ };
+ if self.menu().may_place(source_slot_index, target_item) {
+ let source_max_stack = self.menu().max_stack_size(source_slot_index);
+ if target_slot.count() > source_max_stack as i8 {
+ // if there's more than the max stack size in the target slot
+
+ let target_slot = self.menu_mut().slot_mut(target_slot_index).unwrap();
+ let new_source_slot = target_slot.split(source_max_stack);
+ *self.menu_mut().slot_mut(source_slot_index).unwrap() = new_source_slot;
+ // if !self.inventory_menu.add(new_source_slot) {
+ // player.drop(new_source_slot, true);
+ // }
+ } else {
+ // normal swap
+ let new_target_slot = source_slot.clone();
+ let new_source_slot = target_slot.clone();
+
+ let target_slot = self.menu_mut().slot_mut(target_slot_index).unwrap();
+ *target_slot = new_target_slot;
+
+ let source_slot = self.menu_mut().slot_mut(source_slot_index).unwrap();
+ *source_slot = new_source_slot;
+ }
+ }
+ }
+ }
+ ClickOperation::Clone(CloneClick { slot }) => {
+ if !player_abilities.instant_break || self.carried.is_present() {
+ return;
+ }
+ let Some(source_slot) = self.menu().slot(*slot as usize) else {
+ return;
+ };
+ let ItemSlot::Present(source_item) = source_slot else {
+ return;
+ };
+ let mut new_carried = source_item.clone();
+ new_carried.count = new_carried.kind.max_stack_size();
+ self.carried = ItemSlot::Present(new_carried);
+ }
+ ClickOperation::Throw(c) => {
+ if self.carried.is_present() {
+ return;
+ }
+
+ let (ThrowClick::Single { slot: slot_index }
+ | ThrowClick::All { slot: slot_index }) = c;
+ let slot_index = *slot_index as usize;
+
+ let Some(slot) = self.menu_mut().slot_mut(slot_index) else {
+ return;
+ };
+ let ItemSlot::Present(slot_item) = slot else {
+ return;
+ };
+
+ let dropping_count = match c {
+ ThrowClick::Single { .. } => 1,
+ ThrowClick::All { .. } => slot_item.count,
+ };
+
+ let _dropping = slot_item.split(dropping_count as u8);
+ // player.drop(dropping, true);
+ }
+ ClickOperation::PickupAll(PickupAllClick {
+ slot: source_slot_index,
+ reversed,
+ }) => {
+ let source_slot_index = *source_slot_index as usize;
+
+ let source_slot = self.menu().slot(source_slot_index).unwrap();
+ let target_slot = self.carried.clone();
+
+ if target_slot.is_empty()
+ || (source_slot.is_present() && self.menu().may_pickup(source_slot_index))
+ {
+ return;
+ }
+
+ let ItemSlot::Present(target_slot_item) = &target_slot else {
+ unreachable!("target slot is not empty but is not present");
+ };
+
+ for round in 0..2 {
+ let iterator: Box<dyn Iterator<Item = usize>> = if *reversed {
+ Box::new((0..self.menu().len()).rev())
+ } else {
+ Box::new(0..self.menu().len())
+ };
+
+ for i in iterator {
+ if target_slot_item.count < target_slot_item.kind.max_stack_size() {
+ let checking_slot = self.menu().slot(i).unwrap();
+ if let ItemSlot::Present(checking_item) = checking_slot {
+ if can_item_quick_replace(checking_slot, &target_slot, true)
+ && self.menu().may_pickup(i)
+ && (round != 0
+ || checking_item.count
+ != checking_item.kind.max_stack_size())
+ {
+ // get the checking_slot and checking_item again but mutable
+ let checking_slot = self.menu_mut().slot_mut(i).unwrap();
+
+ let taken_item =
+ checking_slot.split(checking_slot.count() as u8);
+
+ // now extend the carried item
+ let target_slot = &mut self.carried;
+ let ItemSlot::Present(target_slot_item) = target_slot else {
+ unreachable!("target slot is not empty but is not present");
+ };
+ target_slot_item.count += taken_item.count();
+ }
+ }
+ }
+ }
+ }
+ }
+ _ => {}
+ }
+ }
+
+ fn reset_quick_craft(&mut self) {
+ self.quick_craft_status = QuickCraftStatusKind::Start;
+ self.quick_craft_slots.clear();
+ }
+}
+
+fn can_item_quick_replace(
+ target_slot: &ItemSlot,
+ item: &ItemSlot,
+ ignore_item_count: bool,
+) -> bool {
+ let ItemSlot::Present(target_slot) = target_slot else {
+ return false;
+ };
+ let ItemSlot::Present(item) = item else {
+ // i *think* this is what vanilla does
+ // not 100% sure lol probably doesn't matter though
+ return false;
+ };
+
+ if !item.is_same_item_and_nbt(target_slot) {
+ return false;
+ }
+ let count = target_slot.count as u16
+ + if ignore_item_count {
+ 0
+ } else {
+ item.count as u16
+ };
+ count <= item.kind.max_stack_size() as u16
+}
+
+// public static void getQuickCraftSlotCount(Set<Slot> quickCraftSlots, int
+// quickCraftType, ItemStack itemStack, int var3) {
+// switch (quickCraftType) {
+// case 0:
+// itemStack.setCount(Mth.floor((float) itemStack.getCount() / (float)
+// quickCraftSlots.size())); break;
+// case 1:
+// itemStack.setCount(1);
+// break;
+// case 2:
+// itemStack.setCount(itemStack.getItem().getMaxStackSize());
+// }
+
+// itemStack.grow(var3);
+// }
+fn get_quick_craft_slot_count(
+ quick_craft_slots: &HashSet<u16>,
+ quick_craft_kind: &QuickCraftKind,
+ item: &mut ItemSlotData,
+ slot_item_count: i8,
+) {
+ item.count = match quick_craft_kind {
+ QuickCraftKind::Left => item.count / quick_craft_slots.len() as i8,
+ QuickCraftKind::Right => 1,
+ QuickCraftKind::Middle => item.kind.max_stack_size(),
+ };
+ item.count += slot_item_count;
+}
+
+impl Default for InventoryComponent {
+ fn default() -> Self {
+ InventoryComponent {
+ inventory_menu: Menu::Player(azalea_inventory::Player::default()),
+ id: 0,
+ container_menu: None,
+ carried: ItemSlot::Empty,
+ state_id: 0,
+ quick_craft_status: QuickCraftStatusKind::Start,
+ quick_craft_kind: QuickCraftKind::Middle,
+ quick_craft_slots: HashSet::new(),
+ }
+ }
+}
+
+/// Sent from the server when a menu (like a chest or crafting table) was
+/// opened by the client.
+#[derive(Debug)]
+pub struct MenuOpenedEvent {
+ pub entity: Entity,
+ pub window_id: u32,
+ pub menu_type: MenuKind,
+ pub title: FormattedText,
+}
+fn handle_menu_opened_event(
+ mut events: EventReader<MenuOpenedEvent>,
+ mut query: Query<&mut InventoryComponent>,
+) {
+ for event in events.iter() {
+ let mut inventory = query.get_mut(event.entity).unwrap();
+ inventory.id = event.window_id as u8;
+ inventory.container_menu = Some(Menu::from_kind(event.menu_type));
+ }
+}
+
+/// Tell the server that we want to close a container.
+///
+/// Note that this is also sent when the client closes its own inventory, even
+/// though there is no packet for opening its inventory.
+pub struct CloseContainerEvent {
+ pub entity: Entity,
+ /// The ID of the container to close. 0 for the player's inventory. If this
+ /// is not the same as the currently open inventory, nothing will happen.
+ pub id: u8,
+}
+fn handle_container_close_event(
+ mut events: EventReader<CloseContainerEvent>,
+ mut client_side_events: EventWriter<ClientSideCloseContainerEvent>,
+ query: Query<(&LocalPlayer, &InventoryComponent)>,
+) {
+ for event in events.iter() {
+ let (local_player, inventory) = query.get(event.entity).unwrap();
+ if event.id != inventory.id {
+ warn!(
+ "Tried to close container with ID {}, but the current container ID is {}",
+ event.id, inventory.id
+ );
+ continue;
+ }
+
+ local_player.write_packet(
+ ServerboundContainerClosePacket {
+ container_id: inventory.id,
+ }
+ .get(),
+ );
+ client_side_events.send(ClientSideCloseContainerEvent {
+ entity: event.entity,
+ });
+ }
+}
+
+/// Close a container without notifying the server.
+///
+/// Note that this also gets fired when we get a [`CloseContainerEvent`].
+pub struct ClientSideCloseContainerEvent {
+ pub entity: Entity,
+}
+fn handle_client_side_close_container_event(
+ mut events: EventReader<ClientSideCloseContainerEvent>,
+ mut query: Query<&mut InventoryComponent>,
+) {
+ for event in events.iter() {
+ let mut inventory = query.get_mut(event.entity).unwrap();
+ inventory.container_menu = None;
+ inventory.id = 0;
+ }
+}
+
+#[derive(Debug)]
+pub struct ContainerClickEvent {
+ pub entity: Entity,
+ pub window_id: u8,
+ pub operation: ClickOperation,
+}
+fn handle_container_click_event(
+ mut events: EventReader<ContainerClickEvent>,
+ mut query: Query<(&mut InventoryComponent, &LocalPlayer)>,
+) {
+ for event in events.iter() {
+ let (mut inventory, local_player) = 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 {}",
+ event.window_id, inventory.id
+ );
+ continue;
+ }
+
+ let menu = inventory.menu_mut();
+ let old_slots = menu.slots().clone();
+
+ // menu.click(&event.operation);
+
+ // see which slots changed after clicking and put them in the hashmap
+ // the server uses this to check if we desynced
+ let mut changed_slots: HashMap<u16, ItemSlot> = HashMap::new();
+ for (slot_index, old_slot) in old_slots.iter().enumerate() {
+ let new_slot = &menu.slots()[slot_index];
+ if old_slot != new_slot {
+ changed_slots.insert(slot_index as u16, new_slot.clone());
+ }
+ }
+
+ local_player.write_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),
+ button_num: event.operation.button_num(),
+ click_type: event.operation.click_type(),
+ changed_slots,
+ carried_item: inventory.carried.clone(),
+ }
+ .get(),
+ )
+ }
+}
+
+/// Sent from the server when the contents of a container are replaced. Usually
+/// triggered by the `ContainerSetContent` packet.
+pub struct SetContainerContentEvent {
+ pub entity: Entity,
+ pub slots: Vec<ItemSlot>,
+ pub container_id: u8,
+}
+fn handle_set_container_content_event(
+ mut events: EventReader<SetContainerContentEvent>,
+ mut query: Query<&mut InventoryComponent>,
+) {
+ for event in events.iter() {
+ let mut inventory = query.get_mut(event.entity).unwrap();
+
+ if event.container_id != inventory.id {
+ warn!(
+ "Tried to set container content with ID {}, but the current container ID is {}",
+ event.container_id, inventory.id
+ );
+ continue;
+ }
+
+ let menu = inventory.menu_mut();
+ for (i, slot) in event.slots.iter().enumerate() {
+ if let Some(slot_mut) = menu.slot_mut(i) {
+ *slot_mut = slot.clone();
+ }
+ }
+ }
+}
diff --git a/azalea-client/src/lib.rs b/azalea-client/src/lib.rs
index 04fec604..c198ced3 100644
--- a/azalea-client/src/lib.rs
+++ b/azalea-client/src/lib.rs
@@ -18,6 +18,8 @@ pub mod disconnect;
mod entity_query;
mod events;
mod get_mc_dir;
+pub mod interact;
+pub mod inventory;
mod local_player;
mod movement;
pub mod packet_handling;
@@ -28,6 +30,7 @@ pub mod task_pool;
pub use account::{Account, AccountOpts};
pub use client::{
init_ecs_app, start_ecs, Client, ClientInformation, JoinError, JoinedClientBundle, TabList,
+ TickBroadcast,
};
pub use events::Event;
pub use local_player::{GameProfileComponent, LocalPlayer};
diff --git a/azalea-client/src/local_player.rs b/azalea-client/src/local_player.rs
index 540ef3b4..423b4308 100644
--- a/azalea-client/src/local_player.rs
+++ b/azalea-client/src/local_player.rs
@@ -1,14 +1,18 @@
use std::{io, sync::Arc};
use azalea_auth::game_profile::GameProfile;
-use azalea_core::ChunkPos;
+use azalea_core::{ChunkPos, GameMode};
use azalea_protocol::packets::game::ServerboundGamePacket;
use azalea_world::{
- entity::{self, Dead},
- Instance, PartialInstance,
+ entity::{self, Dead, WorldName},
+ Instance, InstanceContainer, PartialInstance,
};
use bevy_ecs::{
- component::Component, entity::Entity, event::EventReader, query::Added, system::Query,
+ component::Component,
+ entity::Entity,
+ event::EventReader,
+ query::Added,
+ system::{Query, Res},
};
use derive_more::{Deref, DerefMut};
use parking_lot::RwLock;
@@ -75,9 +79,17 @@ pub struct GameProfileComponent(pub GameProfile);
/// Marks a [`LocalPlayer`] that's in a loaded chunk. This is updated at the
/// beginning of every tick.
-#[derive(Component)]
+#[derive(Component, Clone, Debug, Copy)]
pub struct LocalPlayerInLoadedChunk;
+/// The gamemode of a local player. For a non-local player, you can look up the
+/// player in the [`TabList`].
+#[derive(Component, Clone, Debug, Copy)]
+pub struct LocalGameMode {
+ pub current: GameMode,
+ pub previous: Option<GameMode>,
+}
+
impl LocalPlayer {
/// Create a new `LocalPlayer`.
pub fn new(
@@ -104,7 +116,7 @@ impl LocalPlayer {
}
/// Write a packet directly to the server.
- pub fn write_packet(&mut self, packet: ServerboundGamePacket) {
+ 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");
@@ -122,16 +134,15 @@ impl Drop for LocalPlayer {
/// Update the [`LocalPlayerInLoadedChunk`] component for all [`LocalPlayer`]s.
pub fn update_in_loaded_chunk(
mut commands: bevy_ecs::system::Commands,
- query: Query<(Entity, &LocalPlayer, &entity::Position)>,
+ query: Query<(Entity, &WorldName, &entity::Position)>,
+ instance_container: Res<InstanceContainer>,
) {
for (entity, local_player, position) in &query {
let player_chunk_pos = ChunkPos::from(position);
- let in_loaded_chunk = local_player
- .world
- .read()
- .chunks
- .get(&player_chunk_pos)
- .is_some();
+ let instance_lock = instance_container
+ .get(local_player)
+ .expect("local player should always be in an instance");
+ let in_loaded_chunk = instance_lock.read().chunks.get(&player_chunk_pos).is_some();
if in_loaded_chunk {
commands.entity(entity).insert(LocalPlayerInLoadedChunk);
} else {
@@ -176,7 +187,7 @@ pub fn handle_send_packet_event(
mut query: Query<&mut LocalPlayer>,
) {
for event in send_packet_events.iter() {
- if let Ok(mut local_player) = query.get_mut(event.entity) {
+ if let Ok(local_player) = query.get_mut(event.entity) {
local_player.write_packet(event.packet.clone());
}
}
diff --git a/azalea-client/src/movement.rs b/azalea-client/src/movement.rs
index f6123c70..d68be8b8 100644
--- a/azalea-client/src/movement.rs
+++ b/azalea-client/src/movement.rs
@@ -16,6 +16,7 @@ use azalea_world::{
};
use bevy_app::{App, CoreSchedule, IntoSystemAppConfigs, Plugin};
use bevy_ecs::{
+ component::Component,
entity::Entity,
event::EventReader,
query::With,
@@ -84,18 +85,26 @@ impl Client {
**jumping_ref
}
- /// Sets your rotation. `y_rot` is yaw (looking to the side), `x_rot` is
- /// pitch (looking up and down). You can get these numbers from the vanilla
- /// f3 screen.
+ /// Sets the direction the client is looking. `y_rot` is yaw (looking to the
+ /// side), `x_rot` is pitch (looking up and down). You can get these
+ /// numbers from the vanilla f3 screen.
/// `y_rot` goes from -180 to 180, and `x_rot` goes from -90 to 90.
- pub fn set_rotation(&mut self, y_rot: f32, x_rot: f32) {
+ pub fn set_direction(&mut self, y_rot: f32, x_rot: f32) {
let mut ecs = self.ecs.lock();
- let mut physics = self.query::<&mut entity::Physics>(&mut ecs);
+ let mut look_direction = self.query::<&mut entity::LookDirection>(&mut ecs);
- entity::set_rotation(&mut physics, y_rot, x_rot);
+ (look_direction.y_rot, look_direction.x_rot) = (y_rot, x_rot);
}
}
+/// A component that contains the look direction that was last sent over the
+/// network.
+#[derive(Debug, Component, Clone, Default)]
+pub struct LastSentLookDirection {
+ pub x_rot: f32,
+ pub y_rot: f32,
+}
+
#[allow(clippy::type_complexity)]
pub(crate) fn send_position(
mut query: Query<
@@ -106,6 +115,8 @@ pub(crate) fn send_position(
&entity::Position,
&mut entity::LastSentPosition,
&mut entity::Physics,
+ &entity::LookDirection,
+ &mut LastSentLookDirection,
&entity::metadata::Sprinting,
),
&LocalPlayerInLoadedChunk,
@@ -118,6 +129,8 @@ pub(crate) fn send_position(
position,
mut last_sent_position,
mut physics,
+ direction,
+ mut last_direction,
sprinting,
) in query.iter_mut()
{
@@ -130,8 +143,8 @@ pub(crate) fn send_position(
let x_delta = position.x - last_sent_position.x;
let y_delta = position.y - last_sent_position.y;
let z_delta = position.z - last_sent_position.z;
- let y_rot_delta = (physics.y_rot - physics.y_rot_last) as f64;
- let x_rot_delta = (physics.x_rot - physics.x_rot_last) as f64;
+ let y_rot_delta = (direction.y_rot - last_direction.y_rot) as f64;
+ let x_rot_delta = (direction.x_rot - last_direction.x_rot) as f64;
physics_state.position_remainder += 1;
@@ -140,19 +153,19 @@ pub(crate) fn send_position(
let sending_position = ((x_delta.powi(2) + y_delta.powi(2) + z_delta.powi(2))
> 2.0e-4f64.powi(2))
|| physics_state.position_remainder >= 20;
- let sending_rotation = y_rot_delta != 0.0 || x_rot_delta != 0.0;
+ let sending_direction = y_rot_delta != 0.0 || x_rot_delta != 0.0;
// if self.is_passenger() {
// TODO: posrot packet for being a passenger
// }
- let packet = if sending_position && sending_rotation {
+ let packet = if sending_position && sending_direction {
Some(
ServerboundMovePlayerPosRotPacket {
x: position.x,
y: position.y,
z: position.z,
- x_rot: physics.x_rot,
- y_rot: physics.y_rot,
+ x_rot: direction.x_rot,
+ y_rot: direction.y_rot,
on_ground: physics.on_ground,
}
.get(),
@@ -167,11 +180,11 @@ pub(crate) fn send_position(
}
.get(),
)
- } else if sending_rotation {
+ } else if sending_direction {
Some(
ServerboundMovePlayerRotPacket {
- x_rot: physics.x_rot,
- y_rot: physics.y_rot,
+ x_rot: direction.x_rot,
+ y_rot: direction.y_rot,
on_ground: physics.on_ground,
}
.get(),
@@ -191,9 +204,9 @@ pub(crate) fn send_position(
**last_sent_position = **position;
physics_state.position_remainder = 0;
}
- if sending_rotation {
- physics.y_rot_last = physics.y_rot;
- physics.x_rot_last = physics.x_rot;
+ if sending_direction {
+ last_direction.y_rot = direction.y_rot;
+ last_direction.x_rot = direction.x_rot;
}
physics.last_on_ground = physics.on_ground;
diff --git a/azalea-client/src/packet_handling.rs b/azalea-client/src/packet_handling.rs
index b9837dba..8ffff870 100644
--- a/azalea-client/src/packet_handling.rs
+++ b/azalea-client/src/packet_handling.rs
@@ -1,6 +1,6 @@
use std::{collections::HashSet, io::Cursor, sync::Arc};
-use azalea_core::{ChunkPos, ResourceLocation, Vec3};
+use azalea_core::{ChunkPos, GameMode, ResourceLocation, Vec3};
use azalea_protocol::{
connect::{ReadConnection, WriteConnection},
packets::game::{
@@ -16,7 +16,7 @@ use azalea_protocol::{
use azalea_world::{
entity::{
metadata::{apply_metadata, Health, PlayerMetadataBundle},
- set_rotation, Dead, EntityBundle, EntityKind, EntityUpdateSet, LastSentPosition,
+ Dead, EntityBundle, EntityKind, EntityUpdateSet, LastSentPosition, LookDirection,
MinecraftEntityId, Physics, PlayerBundle, Position, WorldName,
},
entity::{LoadedBy, RelativeEntityUpdate},
@@ -37,9 +37,13 @@ use tokio::sync::mpsc;
use crate::{
chat::{ChatPacket, ChatReceivedEvent},
- client::TabList,
+ client::{PlayerAbilities, TabList},
disconnect::DisconnectEvent,
- local_player::{GameProfileComponent, LocalPlayer},
+ inventory::{
+ ClientSideCloseContainerEvent, InventoryComponent, MenuOpenedEvent,
+ SetContainerContentEvent,
+ },
+ local_player::{GameProfileComponent, LocalGameMode, LocalPlayer},
ClientInformation, PlayerInfo,
};
@@ -194,7 +198,7 @@ fn process_packet_events(ecs: &mut World) {
)>,
ResMut<InstanceContainer>,
)> = SystemState::new(ecs);
- let (mut commands, mut query, mut world_container) = system_state.get_mut(ecs);
+ let (mut commands, mut query, mut instance_container) = system_state.get_mut(ecs);
let (mut local_player, world_name, game_profile, client_information) =
query.get_mut(player_entity).unwrap();
@@ -220,16 +224,16 @@ fn process_packet_events(ecs: &mut World) {
.entity(player_entity)
.insert(WorldName(new_world_name.clone()));
}
- // add this world to the world_container (or don't if it's already
+ // add this world to the instance_container (or don't if it's already
// there)
- let weak_world = world_container.insert(
+ let weak_world = instance_container.insert(
new_world_name.clone(),
dimension.height,
dimension.min_y,
);
// set the partial_world to an empty world
// (when we add chunks or entities those will be in the
- // world_container)
+ // instance_container)
*local_player.partial_instance.write() = PartialInstance::new(
client_information.view_distance.into(),
@@ -250,9 +254,14 @@ fn process_packet_events(ecs: &mut World) {
metadata: PlayerMetadataBundle::default(),
};
// insert our components into the ecs :)
- commands
- .entity(player_entity)
- .insert((MinecraftEntityId(p.player_id), player_bundle));
+ commands.entity(player_entity).insert((
+ MinecraftEntityId(p.player_id),
+ LocalGameMode {
+ current: p.game_type,
+ previous: p.previous_game_type.into(),
+ },
+ player_bundle,
+ ));
}
// send the client information that we have set
@@ -288,6 +297,12 @@ fn process_packet_events(ecs: &mut World) {
}
ClientboundGamePacket::PlayerAbilities(p) => {
debug!("Got player abilities packet {:?}", p);
+ let mut system_state: SystemState<Query<&mut PlayerAbilities>> =
+ SystemState::new(ecs);
+ let mut query = system_state.get_mut(ecs);
+ let mut player_abilities = query.get_mut(player_entity).unwrap();
+
+ *player_abilities = PlayerAbilities::from(p);
}
ClientboundGamePacket::SetCarriedItem(p) => {
debug!("Got set carried item packet {:?}", p);
@@ -319,16 +334,18 @@ fn process_packet_events(ecs: &mut World) {
// TODO: reply with teleport confirm
debug!("Got player position packet {:?}", p);
+ #[allow(clippy::type_complexity)]
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((mut local_player, mut physics, mut position, mut last_sent_position)) =
+ let Ok((local_player, mut physics, mut direction, mut position, mut last_sent_position)) =
query.get_mut(player_entity) else {
continue;
};
@@ -364,10 +381,10 @@ fn process_packet_events(ecs: &mut World) {
let mut y_rot = p.y_rot;
let mut x_rot = p.x_rot;
if p.relative_arguments.x_rot {
- x_rot += physics.x_rot;
+ x_rot += direction.x_rot;
}
if p.relative_arguments.y_rot {
- y_rot += physics.y_rot;
+ y_rot += direction.y_rot;
}
physics.delta = Vec3 {
@@ -378,7 +395,7 @@ fn process_packet_events(ecs: &mut World) {
// we call a function instead of setting the fields ourself since the
// function makes sure the rotations stay in their
// ranges
- set_rotation(&mut physics, y_rot, x_rot);
+ (direction.y_rot, direction.x_rot) = (y_rot, x_rot);
// TODO: minecraft sets "xo", "yo", and "zo" here but idk what that means
// so investigate that ig
let new_pos = Vec3 {
@@ -633,9 +650,6 @@ fn process_packet_events(ecs: &mut World) {
ClientboundGamePacket::SetDefaultSpawnPosition(p) => {
debug!("Got set default spawn position packet {:?}", p);
}
- ClientboundGamePacket::ContainerSetContent(p) => {
- debug!("Got container set content packet {:?}", p);
- }
ClientboundGamePacket::SetHealth(p) => {
debug!("Got set health packet {:?}", p);
@@ -765,7 +779,7 @@ fn process_packet_events(ecs: &mut World) {
id: p.id,
});
- let mut local_player = query.get_mut(player_entity).unwrap();
+ 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:?}");
}
@@ -831,7 +845,23 @@ 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);
+
+ #[allow(clippy::single_match)]
+ match p.event {
+ EventType::ChangeGameMode => {
+ let mut system_state: SystemState<Query<&mut LocalGameMode>> =
+ SystemState::new(ecs);
+ let mut query = system_state.get_mut(ecs);
+ let mut local_game_mode = query.get_mut(player_entity).unwrap();
+ if let Some(new_game_mode) = GameMode::from_id(p.param as u8) {
+ local_game_mode.current = new_game_mode;
+ }
+ }
+ _ => {}
+ }
}
ClientboundGamePacket::LevelParticles(p) => {
debug!("Got level particles packet {:?}", p);
@@ -855,8 +885,93 @@ fn process_packet_events(ecs: &mut World) {
}
ClientboundGamePacket::BossEvent(_) => {}
ClientboundGamePacket::CommandSuggestions(_) => {}
- ClientboundGamePacket::ContainerSetData(_) => {}
- ClientboundGamePacket::ContainerSetSlot(_) => {}
+ ClientboundGamePacket::ContainerSetContent(p) => {
+ debug!("Got container set content packet {:?}", p);
+
+ let mut system_state: SystemState<(
+ Query<&mut InventoryComponent>,
+ EventWriter<SetContainerContentEvent>,
+ )> = SystemState::new(ecs);
+ let (mut query, mut events) = system_state.get_mut(ecs);
+ let mut inventory = query.get_mut(player_entity).unwrap();
+
+ // container id 0 is always the player's inventory
+ if p.container_id == 0 {
+ // this is just so it has the same type as the `else` block
+ for (i, slot) in p.items.iter().enumerate() {
+ if let Some(slot_mut) = inventory.inventory_menu.slot_mut(i) {
+ *slot_mut = slot.clone();
+ }
+ }
+ } else {
+ events.send(SetContainerContentEvent {
+ entity: player_entity,
+ slots: p.items.clone(),
+ container_id: p.container_id as u8,
+ });
+ }
+ }
+ ClientboundGamePacket::ContainerSetData(p) => {
+ debug!("Got container set data packet {:?}", p);
+ // let mut system_state: SystemState<Query<&mut
+ // InventoryComponent>> =
+ // SystemState::new(ecs);
+ // let mut query = system_state.get_mut(ecs);
+ // let mut inventory =
+ // query.get_mut(player_entity).unwrap();
+
+ // TODO: handle ContainerSetData packet
+ // this is used for various things like the furnace progress
+ // bar
+ // see https://wiki.vg/Protocol#Set_Container_Property
+ }
+ ClientboundGamePacket::ContainerSetSlot(p) => {
+ debug!("Got container set slot packet {:?}", p);
+
+ let mut system_state: SystemState<Query<&mut InventoryComponent>> =
+ SystemState::new(ecs);
+ let mut query = system_state.get_mut(ecs);
+ let mut inventory = query.get_mut(player_entity).unwrap();
+
+ if p.container_id == -1 {
+ // -1 means carried item
+ inventory.carried = p.item_stack.clone();
+ } else if p.container_id == -2 {
+ if let Some(slot) = inventory.inventory_menu.slot_mut(p.slot.into()) {
+ *slot = p.item_stack.clone();
+ }
+ } else {
+ let is_creative_mode_and_inventory_closed = false;
+ // technically minecraft has slightly different behavior here if you're in
+ // creative mode and have your inventory open
+ if p.container_id == 0
+ && azalea_inventory::Player::is_hotbar_slot(p.slot.into())
+ {
+ // minecraft also sets a "pop time" here which is used for an animation
+ // but that's not really necessary
+ if let Some(slot) = inventory.inventory_menu.slot_mut(p.slot.into()) {
+ *slot = p.item_stack.clone();
+ }
+ } else if p.container_id == (inventory.id as i8)
+ && (p.container_id != 0 || !is_creative_mode_and_inventory_closed)
+ {
+ // var2.containerMenu.setItem(var4, var1.getStateId(), var3);
+ if let Some(slot) = inventory.menu_mut().slot_mut(p.slot.into()) {
+ *slot = p.item_stack.clone();
+ inventory.state_id = p.state_id;
+ }
+ }
+ }
+ }
+ ClientboundGamePacket::ContainerClose(_p) => {
+ // there's p.container_id but minecraft doesn't actually check it
+ let mut system_state: SystemState<EventWriter<ClientSideCloseContainerEvent>> =
+ SystemState::new(ecs);
+ let mut client_side_close_container_events = system_state.get_mut(ecs);
+ client_side_close_container_events.send(ClientSideCloseContainerEvent {
+ entity: player_entity,
+ })
+ }
ClientboundGamePacket::Cooldown(_) => {}
ClientboundGamePacket::CustomChatCompletions(_) => {}
ClientboundGamePacket::DeleteChat(_) => {}
@@ -867,7 +982,18 @@ fn process_packet_events(ecs: &mut World) {
ClientboundGamePacket::MerchantOffers(_) => {}
ClientboundGamePacket::MoveVehicle(_) => {}
ClientboundGamePacket::OpenBook(_) => {}
- ClientboundGamePacket::OpenScreen(_) => {}
+ ClientboundGamePacket::OpenScreen(p) => {
+ debug!("Got open screen packet {:?}", p);
+ let mut system_state: SystemState<EventWriter<MenuOpenedEvent>> =
+ SystemState::new(ecs);
+ let mut menu_opened_events = system_state.get_mut(ecs);
+ menu_opened_events.send(MenuOpenedEvent {
+ entity: player_entity,
+ window_id: p.container_id,
+ menu_type: p.menu_type,
+ title: p.title,
+ })
+ }
ClientboundGamePacket::OpenSignEditor(_) => {}
ClientboundGamePacket::Ping(_) => {}
ClientboundGamePacket::PlaceGhostRecipe(_) => {}
@@ -935,7 +1061,6 @@ fn process_packet_events(ecs: &mut World) {
ClientboundGamePacket::TakeItemEntity(_) => {}
ClientboundGamePacket::DisguisedChat(_) => {}
ClientboundGamePacket::UpdateEnabledFeatures(_) => {}
- ClientboundGamePacket::ContainerClose(_) => {}
ClientboundGamePacket::Bundle(_) => {}
ClientboundGamePacket::DamageEvent(_) => {}
ClientboundGamePacket::HurtAnimation(_) => {}
diff --git a/azalea-client/src/player.rs b/azalea-client/src/player.rs
index c2c8a94e..999f2490 100755
--- a/azalea-client/src/player.rs
+++ b/azalea-client/src/player.rs
@@ -1,6 +1,6 @@
use azalea_auth::game_profile::GameProfile;
use azalea_chat::FormattedText;
-use azalea_core::GameType;
+use azalea_core::GameMode;
use azalea_world::entity::EntityInfos;
use bevy_ecs::{
event::EventReader,
@@ -18,7 +18,10 @@ pub struct PlayerInfo {
pub profile: GameProfile,
/// The player's UUID.
pub uuid: Uuid,
- pub gamemode: GameType,
+ /// The current gamemode of the player, like survival or creative.
+ pub gamemode: GameMode,
+ /// The player's latency in milliseconds. The bars in the tab screen depend
+ /// on this.
pub latency: i32,
/// The player's display name in the tab list, but only if it's different
/// from the player's normal username. Use `player_info.profile.name` to get
diff --git a/azalea-core/Cargo.toml b/azalea-core/Cargo.toml
index 6f49774c..76f4b456 100644..100755
--- a/azalea-core/Cargo.toml
+++ b/azalea-core/Cargo.toml
@@ -10,8 +10,12 @@ version = "0.6.0"
[dependencies]
azalea-buf = { path = "../azalea-buf", version = "^0.6.0" }
+azalea-chat = { path = "../azalea-chat", version = "^0.6.0" }
+azalea-inventory = { version = "0.1.0", path = "../azalea-inventory" }
azalea-nbt = { path = "../azalea-nbt", version = "^0.6.0" }
+azalea-registry = { path = "../azalea-registry", version = "^0.6.0" }
bevy_ecs = { version = "0.10.0", default-features = false, optional = true }
+num-traits = "0.2.15"
serde = { version = "^1.0", optional = true }
uuid = "^1.1.2"
diff --git a/azalea-core/src/aabb.rs b/azalea-core/src/aabb.rs
index 58f079e7..7ad4a657 100755
--- a/azalea-core/src/aabb.rs
+++ b/azalea-core/src/aabb.rs
@@ -164,15 +164,15 @@ impl AABB {
}
}
- pub fn move_relative(&self, x: f64, y: f64, z: f64) -> AABB {
+ pub fn move_relative(&self, delta: &Vec3) -> AABB {
AABB {
- min_x: self.min_x + x,
- min_y: self.min_y + y,
- min_z: self.min_z + z,
+ min_x: self.min_x + delta.x,
+ min_y: self.min_y + delta.y,
+ min_z: self.min_z + delta.z,
- max_x: self.max_x + x,
- max_y: self.max_y + y,
- max_z: self.max_z + z,
+ max_x: self.max_x + delta.x,
+ max_y: self.max_y + delta.y,
+ max_z: self.max_z + delta.z,
}
}
@@ -227,12 +227,11 @@ impl AABB {
pub fn clip(&self, min: &Vec3, max: &Vec3) -> Option<Vec3> {
let mut t = 1.0;
let delta = max - min;
- let _dir = self.get_direction(self, min, &mut t, None, &delta)?;
+ let _dir = Self::get_direction(self, min, &mut t, None, &delta)?;
Some(min + &(delta * t))
}
pub fn clip_iterable(
- &self,
boxes: &Vec<AABB>,
from: &Vec3,
to: &Vec3,
@@ -243,7 +242,13 @@ impl AABB {
let delta = to - from;
for aabb in boxes {
- dir = self.get_direction(aabb, from, &mut t, dir, &delta);
+ dir = Self::get_direction(
+ &aabb.move_relative(&pos.to_vec3_floored()),
+ from,
+ &mut t,
+ dir,
+ &delta,
+ );
}
let dir = dir?;
Some(BlockHitResult {
@@ -256,15 +261,14 @@ impl AABB {
}
fn get_direction(
- &self,
aabb: &AABB,
from: &Vec3,
t: &mut f64,
- dir: Option<Direction>,
+ mut dir: Option<Direction>,
delta: &Vec3,
) -> Option<Direction> {
if delta.x > EPSILON {
- return self.clip_point(ClipPointOpts {
+ dir = Self::clip_point(ClipPointOpts {
t,
approach_dir: dir,
delta,
@@ -277,7 +281,7 @@ impl AABB {
start: from,
});
} else if delta.x < -EPSILON {
- return self.clip_point(ClipPointOpts {
+ dir = Self::clip_point(ClipPointOpts {
t,
approach_dir: dir,
delta,
@@ -292,7 +296,7 @@ impl AABB {
}
if delta.y > EPSILON {
- return self.clip_point(ClipPointOpts {
+ dir = Self::clip_point(ClipPointOpts {
t,
approach_dir: dir,
delta: &Vec3 {
@@ -313,7 +317,7 @@ impl AABB {
},
});
} else if delta.y < -EPSILON {
- return self.clip_point(ClipPointOpts {
+ dir = Self::clip_point(ClipPointOpts {
t,
approach_dir: dir,
delta: &Vec3 {
@@ -336,7 +340,7 @@ impl AABB {
}
if delta.z > EPSILON {
- return self.clip_point(ClipPointOpts {
+ dir = Self::clip_point(ClipPointOpts {
t,
approach_dir: dir,
delta: &Vec3 {
@@ -357,7 +361,7 @@ impl AABB {
},
});
} else if delta.z < -EPSILON {
- return self.clip_point(ClipPointOpts {
+ dir = Self::clip_point(ClipPointOpts {
t,
approach_dir: dir,
delta: &Vec3 {
@@ -382,18 +386,18 @@ impl AABB {
dir
}
- fn clip_point(&self, opts: ClipPointOpts) -> Option<Direction> {
- let t_x = (opts.begin - opts.start.x) / opts.delta.x;
- let t_y = (opts.start.y + t_x) / opts.delta.y;
- let t_z = (opts.start.z + t_x) / opts.delta.z;
- if 0.0 < t_x
- && t_x < *opts.t
- && opts.min_x - EPSILON < t_y
- && t_y < opts.max_x + EPSILON
- && opts.min_z - EPSILON < t_z
- && t_z < opts.max_z + EPSILON
+ fn clip_point(opts: ClipPointOpts) -> Option<Direction> {
+ let d = (opts.begin - opts.start.x) / opts.delta.x;
+ let e = opts.start.y + d * opts.delta.y;
+ let f = opts.start.z + d * opts.delta.z;
+ if 0.0 < d
+ && d < *opts.t
+ && opts.min_x - EPSILON < e
+ && e < opts.max_x + EPSILON
+ && opts.min_z - EPSILON < f
+ && f < opts.max_z + EPSILON
{
- *opts.t = t_x;
+ *opts.t = d;
Some(opts.result_dir)
} else {
opts.approach_dir
@@ -435,3 +439,28 @@ impl AABB {
axis.choose(self.min_x, self.min_y, self.min_z)
}
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_aabb_clip_iterable() {
+ assert_ne!(
+ AABB::clip_iterable(
+ &vec![AABB {
+ min_x: 0.,
+ min_y: 0.,
+ min_z: 0.,
+ max_x: 1.,
+ max_y: 1.,
+ max_z: 1.,
+ }],
+ &Vec3::new(-1., -1., -1.),
+ &Vec3::new(1., 1., 1.),
+ &BlockPos::new(0, 0, 0),
+ ),
+ None
+ );
+ }
+}
diff --git a/azalea-core/src/block_hit_result.rs b/azalea-core/src/block_hit_result.rs
index 420d4408..3b4f7257 100755
--- a/azalea-core/src/block_hit_result.rs
+++ b/azalea-core/src/block_hit_result.rs
@@ -1,6 +1,6 @@
use crate::{BlockPos, Direction, Vec3};
-#[derive(Debug, Clone, Copy)]
+#[derive(Debug, Clone, Copy, PartialEq)]
pub struct BlockHitResult {
pub location: Vec3,
pub direction: Direction,
@@ -8,3 +8,22 @@ pub struct BlockHitResult {
pub miss: bool,
pub inside: bool,
}
+
+impl BlockHitResult {
+ pub fn miss(location: Vec3, direction: Direction, block_pos: BlockPos) -> Self {
+ Self {
+ location,
+ direction,
+ block_pos,
+ miss: true,
+ inside: false,
+ }
+ }
+
+ pub fn with_direction(&self, direction: Direction) -> Self {
+ Self { direction, ..*self }
+ }
+ pub fn with_position(&self, block_pos: BlockPos) -> Self {
+ Self { block_pos, ..*self }
+ }
+}
diff --git a/azalea-core/src/direction.rs b/azalea-core/src/direction.rs
index 95dacc69..c872f26c 100755
--- a/azalea-core/src/direction.rs
+++ b/azalea-core/src/direction.rs
@@ -1,6 +1,8 @@
use azalea_buf::McBuf;
-#[derive(Clone, Copy, Debug, McBuf, Default)]
+use crate::Vec3;
+
+#[derive(Clone, Copy, Debug, McBuf, Default, Eq, PartialEq)]
pub enum Direction {
#[default]
Down = 0,
@@ -11,6 +13,54 @@ pub enum Direction {
East,
}
+impl Direction {
+ pub fn nearest(vec: Vec3) -> Direction {
+ let mut best_direction = Direction::North;
+ let mut best_direction_amount = 0.0;
+
+ for dir in [
+ Direction::Down,
+ Direction::Up,
+ Direction::North,
+ Direction::South,
+ Direction::West,
+ Direction::East,
+ ]
+ .iter()
+ {
+ let amount = dir.normal().dot(vec);
+ if amount > best_direction_amount {
+ best_direction = *dir;
+ best_direction_amount = amount;
+ }
+ }
+
+ best_direction
+ }
+
+ pub fn normal(self) -> Vec3 {
+ match self {
+ Direction::Down => Vec3::new(0.0, -1.0, 0.0),
+ Direction::Up => Vec3::new(0.0, 1.0, 0.0),
+ Direction::North => Vec3::new(0.0, 0.0, -1.0),
+ Direction::South => Vec3::new(0.0, 0.0, 1.0),
+ Direction::West => Vec3::new(-1.0, 0.0, 0.0),
+ Direction::East => Vec3::new(1.0, 0.0, 0.0),
+ }
+ }
+
+ pub fn opposite(self) -> Direction {
+ match self {
+ Direction::Down => Direction::Up,
+ Direction::Up => Direction::Down,
+ Direction::North => Direction::South,
+ Direction::South => Direction::North,
+ Direction::West => Direction::East,
+ Direction::East => Direction::West,
+ }
+ }
+}
+
// TODO: make azalea_block use this instead of FacingCardinal
#[derive(Clone, Copy, Debug, McBuf)]
pub enum CardinalDirection {
diff --git a/azalea-core/src/game_type.rs b/azalea-core/src/game_type.rs
index f99a5805..e1a3e19b 100644
--- a/azalea-core/src/game_type.rs
+++ b/azalea-core/src/game_type.rs
@@ -1,8 +1,9 @@
use azalea_buf::{BufReadError, McBufReadable, McBufWritable};
use std::io::{Cursor, Write};
-#[derive(Hash, Copy, Clone, Debug, Default)]
-pub enum GameType {
+/// A Minecraft gamemode, like survival or creative.
+#[derive(Hash, Copy, Clone, Debug, Default, Eq, PartialEq)]
+pub enum GameMode {
#[default]
Survival,
Creative,
@@ -10,30 +11,30 @@ pub enum GameType {
Spectator,
}
-impl GameType {
+impl GameMode {
pub fn to_id(&self) -> u8 {
match self {
- GameType::Survival => 0,
- GameType::Creative => 1,
- GameType::Adventure => 2,
- GameType::Spectator => 3,
+ GameMode::Survival => 0,
+ GameMode::Creative => 1,
+ GameMode::Adventure => 2,
+ GameMode::Spectator => 3,
}
}
/// Get the id of the game type, but return -1 if the game type is invalid.
- pub fn to_optional_id<T: Into<Option<GameType>>>(game_type: T) -> i8 {
+ pub fn to_optional_id<T: Into<Option<GameMode>>>(game_type: T) -> i8 {
match game_type.into() {
Some(game_type) => game_type.to_id() as i8,
None => -1,
}
}
- pub fn from_id(id: u8) -> Option<GameType> {
+ pub fn from_id(id: u8) -> Option<GameMode> {
Some(match id {
- 0 => GameType::Survival,
- 1 => GameType::Creative,
- 2 => GameType::Adventure,
- 3 => GameType::Spectator,
+ 0 => GameMode::Survival,
+ 1 => GameMode::Creative,
+ 2 => GameMode::Adventure,
+ 3 => GameMode::Spectator,
_ => return None,
})
}
@@ -42,7 +43,7 @@ impl GameType {
Some(
match id {
-1 => None,
- id => Some(GameType::from_id(id as u8)?),
+ id => Some(GameMode::from_id(id as u8)?),
}
.into(),
)
@@ -52,10 +53,10 @@ impl GameType {
// TODO: these should be translated
// TranslatableComponent("selectWorld.gameMode." + string2)
match self {
- GameType::Survival => "Survival",
- GameType::Creative => "Creative",
- GameType::Adventure => "Adventure",
- GameType::Spectator => "Spectator",
+ GameMode::Survival => "Survival",
+ GameMode::Creative => "Creative",
+ GameMode::Adventure => "Adventure",
+ GameMode::Spectator => "Spectator",
}
}
@@ -63,32 +64,32 @@ impl GameType {
// TODO: These should be translated TranslatableComponent("gameMode." +
// string2);
match self {
- GameType::Survival => "Survival Mode",
- GameType::Creative => "Creative Mode",
- GameType::Adventure => "Adventure Mode",
- GameType::Spectator => "Spectator Mode",
+ GameMode::Survival => "Survival Mode",
+ GameMode::Creative => "Creative Mode",
+ GameMode::Adventure => "Adventure Mode",
+ GameMode::Spectator => "Spectator Mode",
}
}
- pub fn from_name(name: &str) -> GameType {
+ pub fn from_name(name: &str) -> GameMode {
match name {
- "survival" => GameType::Survival,
- "creative" => GameType::Creative,
- "adventure" => GameType::Adventure,
- "spectator" => GameType::Spectator,
+ "survival" => GameMode::Survival,
+ "creative" => GameMode::Creative,
+ "adventure" => GameMode::Adventure,
+ "spectator" => GameMode::Spectator,
_ => panic!("Unknown game type name: {name}"),
}
}
}
-impl McBufReadable for GameType {
+impl McBufReadable for GameMode {
fn read_from(buf: &mut Cursor<&[u8]>) -> Result<Self, BufReadError> {
let id = u8::read_from(buf)?;
- GameType::from_id(id).ok_or(BufReadError::UnexpectedEnumVariant { id: id as i32 })
+ GameMode::from_id(id).ok_or(BufReadError::UnexpectedEnumVariant { id: id as i32 })
}
}
-impl McBufWritable for GameType {
+impl McBufWritable for GameMode {
fn write_into(&self, buf: &mut impl Write) -> Result<(), std::io::Error> {
u8::write_into(&self.to_id(), buf)
}
@@ -97,15 +98,15 @@ impl McBufWritable for GameType {
/// Rust doesn't let us `impl McBufReadable for Option<GameType>` so we have to
/// make a new type :(
#[derive(Hash, Copy, Clone, Debug)]
-pub struct OptionalGameType(pub Option<GameType>);
+pub struct OptionalGameType(pub Option<GameMode>);
-impl From<Option<GameType>> for OptionalGameType {
- fn from(game_type: Option<GameType>) -> Self {
+impl From<Option<GameMode>> for OptionalGameType {
+ fn from(game_type: Option<GameMode>) -> Self {
OptionalGameType(game_type)
}
}
-impl From<OptionalGameType> for Option<GameType> {
+impl From<OptionalGameType> for Option<GameMode> {
fn from(optional_game_type: OptionalGameType) -> Self {
optional_game_type.0
}
@@ -114,12 +115,12 @@ impl From<OptionalGameType> for Option<GameType> {
impl McBufReadable for OptionalGameType {
fn read_from(buf: &mut Cursor<&[u8]>) -> Result<Self, BufReadError> {
let id = i8::read_from(buf)?;
- GameType::from_optional_id(id).ok_or(BufReadError::UnexpectedEnumVariant { id: id as i32 })
+ GameMode::from_optional_id(id).ok_or(BufReadError::UnexpectedEnumVariant { id: id as i32 })
}
}
impl McBufWritable for OptionalGameType {
fn write_into(&self, buf: &mut impl Write) -> Result<(), std::io::Error> {
- GameType::to_optional_id(*self).write_into(buf)
+ GameMode::to_optional_id(*self).write_into(buf)
}
}
diff --git a/azalea-core/src/lib.rs b/azalea-core/src/lib.rs
index ce502fe5..7bf4a12c 100755
--- a/azalea-core/src/lib.rs
+++ b/azalea-core/src/lib.rs
@@ -13,9 +13,6 @@ pub use resource_location::*;
mod game_type;
pub use game_type::*;
-mod slot;
-pub use slot::*;
-
mod position;
pub use position::*;
@@ -40,6 +37,8 @@ pub use aabb::*;
mod block_hit_result;
pub use block_hit_result::*;
+// some random math things used in minecraft are defined down here
+
// TODO: make this generic
pub fn binary_search(mut min: i32, max: i32, predicate: &dyn Fn(i32) -> bool) -> i32 {
let mut diff = max - min;
@@ -70,6 +69,10 @@ pub fn gcd(mut a: u32, mut b: u32) -> u32 {
a
}
+pub fn lerp<T: num_traits::Float>(amount: T, a: T, b: T) -> T {
+ a + amount * (b - a)
+}
+
#[cfg(test)]
mod tests {
use super::*;
diff --git a/azalea-core/src/particle/mod.rs b/azalea-core/src/particle/mod.rs
index 8dc9f8c6..60128f3f 100755
--- a/azalea-core/src/particle/mod.rs
+++ b/azalea-core/src/particle/mod.rs
@@ -1,5 +1,6 @@
-use crate::{BlockPos, Slot};
+use crate::BlockPos;
use azalea_buf::McBuf;
+use azalea_inventory::ItemSlot;
#[cfg_attr(feature = "bevy_ecs", derive(bevy_ecs::component::Component))]
#[derive(Debug, Clone, McBuf, Default)]
@@ -139,7 +140,7 @@ pub struct DustColorTransitionParticle {
#[derive(Debug, Clone, McBuf)]
pub struct ItemParticle {
- pub item: Slot,
+ pub item: ItemSlot,
}
#[derive(Debug, Clone, McBuf)]
diff --git a/azalea-core/src/position.rs b/azalea-core/src/position.rs
index c09c9966..766c38d6 100755
--- a/azalea-core/src/position.rs
+++ b/azalea-core/src/position.rs
@@ -18,6 +18,12 @@ macro_rules! vec3_impl {
self.x * self.x + self.y * self.y + self.z * self.z
}
+ /// Get the squared distance from this position to another position.
+ /// Equivalent to `(self - other).length_sqr()`.
+ pub fn distance_to_sqr(&self, other: &Self) -> $type {
+ (self - other).length_sqr()
+ }
+
/// Return a new instance of this position with the y coordinate
/// decreased by the given number.
pub fn down(&self, y: $type) -> Self {
@@ -36,6 +42,10 @@ macro_rules! vec3_impl {
z: self.z,
}
}
+
+ pub fn dot(&self, other: Self) -> $type {
+ self.x * other.x + self.y * other.y + self.z * other.z
+ }
}
impl Add for &$name {
@@ -142,6 +152,15 @@ impl BlockPos {
}
}
+ /// Convert the block position into a Vec3 without centering it.
+ pub fn to_vec3_floored(&self) -> Vec3 {
+ Vec3 {
+ x: self.x as f64,
+ y: self.y as f64,
+ z: self.z as f64,
+ }
+ }
+
/// Get the distance of this vector from the origin by doing `x + y + z`.
pub fn length_manhattan(&self) -> u32 {
(self.x.abs() + self.y.abs() + self.z.abs()) as u32
diff --git a/azalea-inventory/Cargo.toml b/azalea-inventory/Cargo.toml
new file mode 100644
index 00000000..4b00901f
--- /dev/null
+++ b/azalea-inventory/Cargo.toml
@@ -0,0 +1,12 @@
+[package]
+edition = "2021"
+name = "azalea-inventory"
+version = "0.1.0"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+azalea-buf = { version = "0.6.0", path = "../azalea-buf" }
+azalea-inventory-macros = { version = "0.1.0", path = "./azalea-inventory-macros" }
+azalea-nbt = { version = "0.6.0", path = "../azalea-nbt" }
+azalea-registry = { version = "0.6.0", path = "../azalea-registry" }
diff --git a/azalea-inventory/README.md b/azalea-inventory/README.md
new file mode 100644
index 00000000..67030f6a
--- /dev/null
+++ b/azalea-inventory/README.md
@@ -0,0 +1,2 @@
+Representations of various inventory data structures in Minecraft.
+
diff --git a/azalea-inventory/azalea-inventory-macros/Cargo.toml b/azalea-inventory/azalea-inventory-macros/Cargo.toml
new file mode 100644
index 00000000..9ac1cd68
--- /dev/null
+++ b/azalea-inventory/azalea-inventory-macros/Cargo.toml
@@ -0,0 +1,14 @@
+[package]
+name = "azalea-inventory-macros"
+version = "0.1.0"
+edition = "2021"
+
+[lib]
+proc-macro = true
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+proc-macro2 = "1.0.47"
+quote = "1.0.21"
+syn = "1.0.104"
diff --git a/azalea-inventory/azalea-inventory-macros/src/lib.rs b/azalea-inventory/azalea-inventory-macros/src/lib.rs
new file mode 100644
index 00000000..d3faa091
--- /dev/null
+++ b/azalea-inventory/azalea-inventory-macros/src/lib.rs
@@ -0,0 +1,45 @@
+mod location_enum;
+mod menu_enum;
+mod menu_impl;
+mod parse_macro;
+mod utils;
+
+use parse_macro::{DeclareMenus, Field};
+use proc_macro::TokenStream;
+use proc_macro2::Span;
+use quote::quote;
+use syn::{self, parse_macro_input, Ident};
+
+#[proc_macro]
+pub fn declare_menus(input: TokenStream) -> TokenStream {
+ let mut input = parse_macro_input!(input as DeclareMenus);
+
+ // implicitly add a `player` field at the end unless an `inventory` field
+ // is present
+ for menu in &mut input.menus {
+ let mut inventory_field_missing = true;
+ for field in &menu.fields {
+ if matches!(field.name.to_string().as_str(), "inventory" | "player") {
+ inventory_field_missing = false;
+ }
+ }
+ if inventory_field_missing {
+ menu.fields.push(Field {
+ name: Ident::new("player", Span::call_site()),
+ length: 36,
+ })
+ }
+ }
+
+ let menu_enum = menu_enum::generate(&input);
+ let menu_impl = menu_impl::generate(&input);
+ let location_enum = location_enum::generate(&input);
+
+ quote! {
+ #menu_enum
+ #menu_impl
+
+ #location_enum
+ }
+ .into()
+}
diff --git a/azalea-inventory/azalea-inventory-macros/src/location_enum.rs b/azalea-inventory/azalea-inventory-macros/src/location_enum.rs
new file mode 100644
index 00000000..6cec88e6
--- /dev/null
+++ b/azalea-inventory/azalea-inventory-macros/src/location_enum.rs
@@ -0,0 +1,59 @@
+use crate::{parse_macro::DeclareMenus, utils::to_pascal_case};
+use proc_macro2::TokenStream;
+use quote::quote;
+use syn::Ident;
+
+pub fn generate(input: &DeclareMenus) -> TokenStream {
+ // pub enum MenuLocation {
+ // Player(PlayerMenuLocation),
+ // ...
+ // }
+ // pub enum PlayerMenuLocation {
+ // CraftResult,
+ // Craft,
+ // Armor,
+ // Inventory,
+ // Offhand,
+ // }
+ // ...
+
+ let mut menu_location_variants = quote! {};
+ let mut enums = quote! {};
+ for menu in &input.menus {
+ let name_snake_case = &menu.name;
+ let variant_name = Ident::new(
+ &to_pascal_case(&name_snake_case.to_string()),
+ name_snake_case.span(),
+ );
+ let enum_name = Ident::new(
+ &format!("{}MenuLocation", variant_name),
+ variant_name.span(),
+ );
+ menu_location_variants.extend(quote! {
+ #variant_name(#enum_name),
+ });
+ let mut individual_menu_location_variants = quote! {};
+ for field in &menu.fields {
+ let field_name = &field.name;
+ let variant_name =
+ Ident::new(&to_pascal_case(&field_name.to_string()), field_name.span());
+ individual_menu_location_variants.extend(quote! {
+ #variant_name,
+ });
+ }
+ enums.extend(quote! {
+ #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
+ pub enum #enum_name {
+ #individual_menu_location_variants
+ }
+ });
+ }
+
+ quote! {
+ pub enum MenuLocation {
+ #menu_location_variants
+ }
+
+ #enums
+ }
+}
diff --git a/azalea-inventory/azalea-inventory-macros/src/menu_enum.rs b/azalea-inventory/azalea-inventory-macros/src/menu_enum.rs
new file mode 100644
index 00000000..a9e4f430
--- /dev/null
+++ b/azalea-inventory/azalea-inventory-macros/src/menu_enum.rs
@@ -0,0 +1,70 @@
+//! Generate the `enum menu` and nothing else. Implementations are in
+//! impl_menu.rs
+
+use crate::parse_macro::{DeclareMenus, Field, Menu};
+use proc_macro2::TokenStream;
+use quote::quote;
+
+pub fn generate(input: &DeclareMenus) -> TokenStream {
+ let mut variants = quote! {};
+ let mut player_fields = None;
+ for menu in &input.menus {
+ if menu.name == "Player" {
+ player_fields = Some(generate_fields(&menu.fields, true));
+ } else {
+ variants.extend(generate_variant_for_menu(menu));
+ }
+ }
+ let player_fields = player_fields.expect("Player variant must be present");
+
+ quote! {
+ #[derive(Clone, Debug, Default)]
+ pub struct Player {
+ #player_fields
+ }
+
+ /// A menu, which is a fixed collection of slots.
+ #[derive(Clone, Debug)]
+ pub enum Menu {
+ Player(Player),
+ #variants
+ }
+ }
+}
+
+/// Player {
+/// craft_result: ItemSlot,
+/// craft: [ItemSlot; 4],
+/// armor: [ItemSlot; 4],
+/// inventory: [ItemSlot; 36],
+/// offhand: ItemSlot,
+/// },
+fn generate_variant_for_menu(menu: &Menu) -> TokenStream {
+ let name = &menu.name;
+ let fields = generate_fields(&menu.fields, false);
+
+ quote! {
+ #name {
+ #fields
+ },
+ }
+}
+
+fn generate_fields(fields: &[Field], public: bool) -> TokenStream {
+ let mut generated_fields = quote! {};
+ for field in fields {
+ let field_length = field.length;
+ let field_type = if field.length == 1 {
+ quote! { ItemSlot }
+ } else {
+ quote! { SlotList<#field_length> }
+ };
+ let field_name = &field.name;
+ if public {
+ generated_fields.extend(quote! { pub #field_name: #field_type, })
+ } else {
+ generated_fields.extend(quote! { #field_name: #field_type, })
+ }
+ }
+ generated_fields
+}
diff --git a/azalea-inventory/azalea-inventory-macros/src/menu_impl.rs b/azalea-inventory/azalea-inventory-macros/src/menu_impl.rs
new file mode 100644
index 00000000..804f69f2
--- /dev/null
+++ b/azalea-inventory/azalea-inventory-macros/src/menu_impl.rs
@@ -0,0 +1,448 @@
+use crate::{
+ parse_macro::{DeclareMenus, Menu},
+ utils::{to_pascal_case, to_snake_case},
+};
+use proc_macro2::TokenStream;
+use quote::quote;
+use syn::Ident;
+
+pub fn generate(input: &DeclareMenus) -> TokenStream {
+ let mut slot_mut_match_variants = quote! {};
+ let mut slot_match_variants = quote! {};
+ let mut len_match_variants = quote! {};
+ let mut kind_match_variants = quote! {};
+ let mut slots_match_variants = quote! {};
+ let mut contents_match_variants = quote! {};
+ let mut location_match_variants = quote! {};
+ let mut player_slots_range_match_variants = quote! {};
+
+ let mut player_consts = quote! {};
+ let mut menu_consts = quote! {};
+
+ let mut hotbar_slots_start = 0;
+ let mut hotbar_slots_end = 0;
+ let mut inventory_without_hotbar_slots_start = 0;
+ let mut inventory_without_hotbar_slots_end = 0;
+
+ for menu in &input.menus {
+ slot_mut_match_variants.extend(generate_match_variant_for_slot_mut(menu, true));
+ slot_match_variants.extend(generate_match_variant_for_slot_mut(menu, false));
+ len_match_variants.extend(generate_match_variant_for_len(menu));
+ kind_match_variants.extend(generate_match_variant_for_kind(menu));
+ slots_match_variants.extend(generate_match_variant_for_slots(menu));
+ contents_match_variants.extend(generate_match_variant_for_contents(menu));
+ location_match_variants.extend(generate_match_variant_for_location(menu));
+ player_slots_range_match_variants
+ .extend(generate_match_variant_for_player_slots_range(menu));
+
+ // this part is only used to generate `Player::is_hotbar_slot`
+ if menu.name == "Player" {
+ let mut i = 0;
+ for field in &menu.fields {
+ let field_name = &field.name;
+ let start = i;
+ i += field.length;
+ let end = i - 1;
+
+ if field_name == "inventory" {
+ // it only subtracts 8 here since it's inclusive (there's 9 total hotbar slots)
+ hotbar_slots_start = end - 8;
+ hotbar_slots_end = end;
+
+ inventory_without_hotbar_slots_start = start;
+ inventory_without_hotbar_slots_end = end - 9;
+ }
+
+ if start == end {
+ let const_name = Ident::new(
+ &format!("{}_SLOT", field_name.to_string().to_uppercase()),
+ field_name.span(),
+ );
+ player_consts.extend(quote! {
+ pub const #const_name: usize = #start;
+ });
+ } else {
+ let const_name = Ident::new(
+ &format!("{}_SLOTS", field_name.to_string().to_uppercase()),
+ field_name.span(),
+ );
+ player_consts.extend(quote! {
+ pub const #const_name: RangeInclusive<usize> = #start..=#end;
+ });
+ }
+ }
+ } else {
+ menu_consts.extend(generate_menu_consts(menu));
+ }
+ }
+
+ assert!(hotbar_slots_start != 0 && hotbar_slots_end != 0);
+ quote! {
+ impl Player {
+ pub const HOTBAR_SLOTS: RangeInclusive<usize> = #hotbar_slots_start..=#hotbar_slots_end;
+ pub const INVENTORY_WITHOUT_HOTBAR_SLOTS: RangeInclusive<usize> = #inventory_without_hotbar_slots_start..=#inventory_without_hotbar_slots_end;
+ #player_consts
+
+ /// Returns whether the given protocol index is in the player's hotbar.
+ ///
+ /// Equivalent to `Player::HOTBAR_SLOTS.contains(&i)`.
+ pub fn is_hotbar_slot(i: usize) -> bool {
+ Self::HOTBAR_SLOTS.contains(&i)
+ }
+ }
+
+ impl Menu {
+ #menu_consts
+
+ /// Get a mutable reference to the [`ItemSlot`] at the given protocol index.
+ ///
+ /// If you're trying to get an item in a menu without caring about
+ /// protocol indexes, you should just `match` it and index the
+ /// [`ItemSlot`] you get.
+ ///
+ /// Use [`Menu::slot`] if you don't need a mutable reference to the slot.
+ ///
+ /// # Errors
+ ///
+ /// Returns `None` if the index is out of bounds.
+ #[inline]
+ pub fn slot_mut(&mut self, i: usize) -> Option<&mut ItemSlot> {
+ Some(match self {
+ #slot_mut_match_variants
+ })
+ }
+
+ /// Get a reference to the [`ItemSlot`] at the given protocol index.
+ ///
+ /// If you're trying to get an item in a menu without caring about
+ /// protocol indexes, you should just `match` it and index the
+ /// [`ItemSlot`] you get.
+ ///
+ /// Use [`Menu::slot_mut`] if you need a mutable reference to the slot.
+ ///
+ /// # Errors
+ ///
+ /// Returns `None` if the index is out of bounds.
+ pub fn slot(&self, i: usize) -> Option<&ItemSlot> {
+ Some(match self {
+ #slot_match_variants
+ })
+ }
+
+ /// Returns the number of slots in the menu.
+ #[allow(clippy::len_without_is_empty)]
+ pub const fn len(&self) -> usize {
+ match self {
+ #len_match_variants
+ }
+ }
+
+ pub fn from_kind(kind: azalea_registry::MenuKind) -> Self {
+ match kind {
+ #kind_match_variants
+ }
+ }
+
+ /// Return the contents of the menu, including the player's inventory.
+ ///
+ /// The indexes in this will match up with [`Menu::slot_mut`].
+ ///
+ /// If you don't want to include the player's inventory, use [`Menu::contents`] instead.
+ pub fn slots(&self) -> Vec<ItemSlot> {
+ match self {
+ #slots_match_variants
+ }
+ }
+
+ /// Return the contents of the menu, not including the player's inventory.
+ ///
+ /// If you want to include the player's inventory, use [`Menu::slots`] instead.
+ pub fn contents(&self) -> Vec<ItemSlot> {
+ match self {
+ #contents_match_variants
+ }
+ }
+
+ pub fn location_for_slot(&self, i: usize) -> Option<MenuLocation> {
+ Some(match self {
+ #location_match_variants
+ })
+ }
+
+ /// Get the range of slot indexes that contain the player's inventory. This may be different for each menu.
+ pub fn player_slots_range(&self) -> RangeInclusive<usize> {
+ match self {
+ #player_slots_range_match_variants
+ }
+ }
+
+ /// Get the range of slot indexes that contain the player's hotbar. This may be different for each menu.
+ pub fn hotbar_slots_range(&self) -> RangeInclusive<usize> {
+ // hotbar is always last 9 slots in the player's inventory
+ ((*self.player_slots_range().end() - 8)..=*self.player_slots_range().end())
+ }
+
+ /// Get the range of slot indexes that contain the player's inventory, not including the hotbar. This may be different for each menu.
+ pub fn player_slots_without_hotbar_range(&self) -> RangeInclusive<usize> {
+ (*self.player_slots_range().start()..=*self.player_slots_range().end() - 9)
+ }
+
+ /// Returns whether the given index would be in the player's hotbar.
+ ///
+ /// Equivalent to `self.hotbar_slots_range().contains(&i)`.
+ pub fn is_hotbar_slot(&self, i: usize) -> bool {
+ self.hotbar_slots_range().contains(&i)
+ }
+ }
+ }
+}
+
+/// Menu::Player {
+/// craft_result,
+/// craft,
+/// armor,
+/// inventory,
+/// offhand,
+/// } => {
+/// match i {
+/// 0 => craft_result,
+/// 1..=4 => craft,
+/// 5..=8 => armor,
+/// // ...
+/// _ => return None,
+/// }
+/// } // ...
+pub fn generate_match_variant_for_slot_mut(menu: &Menu, mutable: bool) -> TokenStream {
+ let mut match_arms = quote! {};
+ let mut i = 0;
+ for field in &menu.fields {
+ let field_name = &field.name;
+ let start = i;
+ i += field.length;
+ let end = i - 1;
+ match_arms.extend(if start == end {
+ quote! { #start => #field_name, }
+ } else if start == 0 {
+ if mutable {
+ quote! { #start..=#end => &mut #field_name[i], }
+ } else {
+ quote! { #start..=#end => &#field_name[i], }
+ }
+ } else if mutable {
+ quote! { #start..=#end => &mut #field_name[i - #start], }
+ } else {
+ quote! { #start..=#end => &#field_name[i - #start], }
+ });
+ }
+
+ generate_matcher(
+ menu,
+ &quote! {
+ match i {
+ #match_arms
+ _ => return None
+ }
+ },
+ true,
+ )
+}
+
+pub fn generate_match_variant_for_len(menu: &Menu) -> TokenStream {
+ let length = menu.fields.iter().map(|f| f.length).sum::<usize>();
+ generate_matcher(
+ menu,
+ &quote! {
+ #length
+ },
+ false,
+ )
+}
+
+pub fn generate_match_variant_for_kind(menu: &Menu) -> TokenStream {
+ // azalea_registry::MenuKind::Generic9x3 => Menu::Generic9x3 { contents:
+ // Default::default(), player: Default::default() },
+
+ let menu_name = &menu.name;
+ let menu_field_names = if menu.name == "Player" {
+ // player isn't in MenuKind
+ return quote! {};
+ } else {
+ let mut menu_field_names = quote! {};
+ for field in &menu.fields {
+ let field_name = &field.name;
+ menu_field_names.extend(quote! { #field_name: Default::default(), })
+ }
+ quote! { { #menu_field_names } }
+ };
+
+ quote! {
+ azalea_registry::MenuKind::#menu_name => Menu::#menu_name #menu_field_names,
+ }
+}
+
+pub fn generate_match_variant_for_slots(menu: &Menu) -> TokenStream {
+ let mut instructions = quote! {};
+ let mut length = 0;
+ for field in &menu.fields {
+ let field_name = &field.name;
+ instructions.extend(if field.length == 1 {
+ quote! { items.push(#field_name.clone()); }
+ } else {
+ quote! { items.extend(#field_name.iter().cloned()); }
+ });
+ length += field.length;
+ }
+
+ generate_matcher(
+ menu,
+ &quote! {
+ let mut items = Vec::with_capacity(#length);
+ #instructions
+ items
+ },
+ true,
+ )
+}
+
+pub fn generate_match_variant_for_contents(menu: &Menu) -> TokenStream {
+ let mut instructions = quote! {};
+ let mut length = 0;
+ for field in &menu.fields {
+ let field_name = &field.name;
+ if field_name == "player" {
+ continue;
+ }
+ instructions.extend(if field.length == 1 {
+ quote! { items.push(#field_name.clone()); }
+ } else {
+ quote! { items.extend(#field_name.iter().cloned()); }
+ });
+ length += field.length;
+ }
+
+ generate_matcher(
+ menu,
+ &quote! {
+ let mut items = Vec::with_capacity(#length);
+ #instructions
+ items
+ },
+ true,
+ )
+}
+
+pub fn generate_match_variant_for_location(menu: &Menu) -> TokenStream {
+ let mut match_arms = quote! {};
+ let mut i = 0;
+
+ let menu_name = Ident::new(&to_pascal_case(&menu.name.to_string()), menu.name.span());
+ let menu_enum_name = Ident::new(&format!("{menu_name}MenuLocation"), menu_name.span());
+
+ for field in &menu.fields {
+ let field_name = Ident::new(&to_pascal_case(&field.name.to_string()), field.name.span());
+ let start = i;
+ i += field.length;
+ let end = i - 1;
+ match_arms.extend(if start == end {
+ quote! { #start => #menu_enum_name::#field_name, }
+ } else {
+ quote! { #start..=#end => #menu_enum_name::#field_name, }
+ });
+ }
+
+ generate_matcher(
+ menu,
+ &quote! {
+ MenuLocation::#menu_name(match i {
+ #match_arms
+ _ => return None
+ })
+ },
+ false,
+ )
+}
+
+pub fn generate_match_variant_for_player_slots_range(menu: &Menu) -> TokenStream {
+ // Menu::Player(Player { .. }) => Player::INVENTORY_SLOTS_RANGE,,
+ // Menu::Generic9x3 { .. } => Menu::GENERIC9X3_SLOTS_RANGE,
+ // ..
+
+ match menu.name.to_string().as_str() {
+ "Player" => {
+ quote! {
+ Menu::Player(Player { .. }) => Player::INVENTORY_SLOTS,
+ }
+ }
+ _ => {
+ let menu_name = &menu.name;
+ let menu_slots_range_name = Ident::new(
+ &format!(
+ "{}_PLAYER_SLOTS",
+ to_snake_case(&menu.name.to_string()).to_uppercase()
+ ),
+ menu.name.span(),
+ );
+ quote! {
+ Menu::#menu_name { .. } => Menu::#menu_slots_range_name,
+ }
+ }
+ }
+}
+
+fn generate_menu_consts(menu: &Menu) -> TokenStream {
+ let mut menu_consts = quote! {};
+
+ let mut i = 0;
+
+ for field in &menu.fields {
+ let field_name_start = format!(
+ "{}_{}",
+ to_snake_case(&menu.name.to_string()).to_uppercase(),
+ to_snake_case(&field.name.to_string()).to_uppercase()
+ );
+ let field_index_start = i;
+ i += field.length;
+ let field_index_end = i - 1;
+
+ if field.length == 1 {
+ let field_name = Ident::new(
+ format!("{}_SLOT", field_name_start).as_str(),
+ field.name.span(),
+ );
+ menu_consts.extend(quote! { pub const #field_name: usize = #field_index_start; });
+ } else {
+ let field_name = Ident::new(
+ format!("{}_SLOTS", field_name_start).as_str(),
+ field.name.span(),
+ );
+ menu_consts.extend(quote! { pub const #field_name: RangeInclusive<usize> = #field_index_start..=#field_index_end; });
+ }
+ }
+
+ menu_consts
+}
+
+pub fn generate_matcher(menu: &Menu, match_arms: &TokenStream, needs_fields: bool) -> TokenStream {
+ let menu_name = &menu.name;
+ let menu_field_names = if needs_fields {
+ let mut menu_field_names = quote! {};
+ for field in &menu.fields {
+ let field_name = &field.name;
+ menu_field_names.extend(quote! { #field_name, })
+ }
+ menu_field_names
+ } else {
+ quote! { .. }
+ };
+
+ let matcher = if menu.name == "Player" {
+ quote! { (Player { #menu_field_names }) }
+ } else {
+ quote! { { #menu_field_names } }
+ };
+ quote! {
+ Menu::#menu_name #matcher => {
+ #match_arms
+ },
+ }
+}
diff --git a/azalea-inventory/azalea-inventory-macros/src/parse_macro.rs b/azalea-inventory/azalea-inventory-macros/src/parse_macro.rs
new file mode 100644
index 00000000..8eada4ec
--- /dev/null
+++ b/azalea-inventory/azalea-inventory-macros/src/parse_macro.rs
@@ -0,0 +1,69 @@
+use syn::{
+ self, braced,
+ parse::{Parse, ParseStream, Result},
+ Ident, LitInt, Token,
+};
+
+/// An identifier, colon, and number
+/// `craft_result: 1`
+pub struct Field {
+ pub name: Ident,
+ pub length: usize,
+}
+impl Parse for Field {
+ fn parse(input: ParseStream) -> Result<Self> {
+ let name = input.parse::<Ident>()?;
+ let _ = input.parse::<Token![:]>()?;
+ let length = input.parse::<LitInt>()?.base10_parse()?;
+ Ok(Self { name, length })
+ }
+}
+
+/// An identifier and a list of `Field` in curly brackets
+/// ```rust,ignore
+/// Player {
+/// craft_result: 1,
+/// ...
+/// }
+/// ```
+pub struct Menu {
+ /// The menu name, e.g. `Player`
+ pub name: Ident,
+ pub fields: Vec<Field>,
+}
+
+impl Parse for Menu {
+ fn parse(input: ParseStream) -> Result<Self> {
+ let name = input.parse::<Ident>()?;
+
+ let content;
+ braced!(content in input);
+ let fields = content
+ .parse_terminated::<Field, Token![,]>(Field::parse)?
+ .into_iter()
+ .collect();
+
+ Ok(Self { name, fields })
+ }
+}
+
+/// A list of `Menu`s
+/// ```rust,ignore
+/// Player {
+/// craft_result: 1,
+/// ...
+/// },
+/// ...
+/// ```
+pub struct DeclareMenus {
+ pub menus: Vec<Menu>,
+}
+impl Parse for DeclareMenus {
+ fn parse(input: ParseStream) -> Result<Self> {
+ let menus = input
+ .parse_terminated::<Menu, Token![,]>(Menu::parse)?
+ .into_iter()
+ .collect();
+ Ok(Self { menus })
+ }
+}
diff --git a/azalea-inventory/azalea-inventory-macros/src/utils.rs b/azalea-inventory/azalea-inventory-macros/src/utils.rs
new file mode 100644
index 00000000..568c9f71
--- /dev/null
+++ b/azalea-inventory/azalea-inventory-macros/src/utils.rs
@@ -0,0 +1,54 @@
+pub fn to_pascal_case(s: &str) -> String {
+ // we get the first item later so this is to make it impossible for that
+ // to error
+ if s.is_empty() {
+ return String::new();
+ }
+
+ let mut result = String::new();
+ let mut prev_was_underscore = true; // set to true by default so the first character is capitalized
+ if s.chars().next().unwrap().is_numeric() {
+ result.push('_');
+ }
+ for c in s.chars() {
+ if c == '_' {
+ prev_was_underscore = true;
+ } else if prev_was_underscore {
+ result.push(c.to_ascii_uppercase());
+ prev_was_underscore = false;
+ } else {
+ result.push(c);
+ }
+ }
+ result
+}
+
+pub fn to_snake_case(s: &str) -> String {
+ let mut result = String::new();
+ let mut prev_was_uppercase = true;
+ for c in s.chars() {
+ if c.is_ascii_uppercase() {
+ if !prev_was_uppercase {
+ result.push('_');
+ }
+ result.push(c.to_ascii_lowercase());
+ prev_was_uppercase = true;
+ } else {
+ result.push(c);
+ prev_was_uppercase = false;
+ }
+ }
+ result
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_snake_case() {
+ assert_eq!(to_snake_case("HelloWorld"), "hello_world");
+ assert_eq!(to_snake_case("helloWorld"), "hello_world");
+ assert_eq!(to_snake_case("hello_world"), "hello_world");
+ }
+}
diff --git a/azalea-inventory/src/item/mod.rs b/azalea-inventory/src/item/mod.rs
new file mode 100644
index 00000000..07e51363
--- /dev/null
+++ b/azalea-inventory/src/item/mod.rs
@@ -0,0 +1,21 @@
+pub trait MaxStackSizeExt {
+ /// Get the maximum stack size for this item.
+ ///
+ /// This is a signed integer to be consistent with the `count` field of
+ /// [`ItemSlotData`].
+ fn max_stack_size(&self) -> i8;
+
+ /// Whether this item can be stacked with other items.
+ ///
+ /// This is equivalent to `self.max_stack_size() > 1`.
+ fn stackable(&self) -> bool {
+ self.max_stack_size() > 1
+ }
+}
+
+impl MaxStackSizeExt for azalea_registry::Item {
+ fn max_stack_size(&self) -> i8 {
+ // TODO: have the properties for every item defined somewhere
+ 64
+ }
+}
diff --git a/azalea-inventory/src/lib.rs b/azalea-inventory/src/lib.rs
new file mode 100644
index 00000000..518c7a1d
--- /dev/null
+++ b/azalea-inventory/src/lib.rs
@@ -0,0 +1,172 @@
+#![doc = include_str!("../README.md")]
+
+pub mod item;
+pub mod operations;
+mod slot;
+
+use std::ops::{Deref, DerefMut, RangeInclusive};
+
+use azalea_inventory_macros::declare_menus;
+pub use slot::{ItemSlot, ItemSlotData};
+
+// TODO: remove this here and in azalea-inventory-macros when rust makes
+// Default be implemented for all array sizes (since right now it's only up to
+// 32)
+
+/// A fixed-size list of [`ItemSlot`]s.
+#[derive(Debug, Clone)]
+pub struct SlotList<const N: usize>([ItemSlot; N]);
+impl<const N: usize> Deref for SlotList<N> {
+ type Target = [ItemSlot; N];
+ fn deref(&self) -> &Self::Target {
+ &self.0
+ }
+}
+impl<const N: usize> DerefMut for SlotList<N> {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.0
+ }
+}
+impl<const N: usize> Default for SlotList<N> {
+ fn default() -> Self {
+ SlotList([(); N].map(|_| ItemSlot::Empty))
+ }
+}
+
+impl Menu {
+ /// Get the [`Player`] from this [`Menu`].
+ ///
+ /// # Panics
+ ///
+ /// Will panic if the menu isn't `Menu::Player`.
+ pub fn as_player(&self) -> &Player {
+ if let Menu::Player(player) = &self {
+ player
+ } else {
+ unreachable!("Called `Menu::as_player` on a menu that wasn't `Player`.")
+ }
+ }
+}
+
+// the player inventory part is always the last 36 slots (except in the Player
+// menu), so we don't have to explicitly specify it
+
+// Client {
+// ...
+// pub menu: Menu,
+// pub inventory: Arc<[Slot; 36]>
+// }
+
+// Generate a `struct Player`, `enum Menu`, and `impl Menu`.
+// a "player" field gets implicitly added with the player inventory
+
+declare_menus! {
+ Player {
+ craft_result: 1,
+ craft: 4,
+ armor: 4,
+ inventory: 36,
+ offhand: 1,
+ },
+ Generic9x1 {
+ contents: 9,
+ },
+ Generic9x2 {
+ contents: 18,
+ },
+ Generic9x3 {
+ contents: 27,
+ },
+ Generic9x4 {
+ contents: 36,
+ },
+ Generic9x5 {
+ contents: 45,
+ },
+ Generic9x6 {
+ contents: 54,
+ },
+ Generic3x3 {
+ contents: 9,
+ },
+ Anvil {
+ first: 1,
+ second: 1,
+ result: 1,
+ },
+ Beacon {
+ payment: 1,
+ },
+ BlastFurnace {
+ ingredient: 1,
+ fuel: 1,
+ result: 1,
+ },
+ BrewingStand {
+ bottles: 3,
+ ingredient: 1,
+ fuel: 1,
+ },
+ Crafting {
+ result: 1,
+ grid: 9,
+ },
+ Enchantment {
+ item: 1,
+ lapis: 1,
+ },
+ Furnace {
+ ingredient: 1,
+ fuel: 1,
+ result: 1,
+ },
+ Grindstone {
+ input: 1,
+ additional: 1,
+ result: 1,
+ },
+ Hopper {
+ contents: 5,
+ },
+ Lectern {
+ book: 1,
+ },
+ Loom {
+ banner: 1,
+ dye: 1,
+ pattern: 1,
+ result: 1,
+ },
+ Merchant {
+ payments: 2,
+ result: 1,
+ },
+ ShulkerBox {
+ contents: 27,
+ },
+ LegacySmithing {
+ input: 1,
+ additional: 1,
+ result: 1,
+ },
+ Smithing {
+ template: 1,
+ base: 1,
+ additional: 1,
+ result: 1,
+ },
+ Smoker {
+ ingredient: 1,
+ fuel: 1,
+ result: 1,
+ },
+ CartographyTable {
+ map: 1,
+ additional: 1,
+ result: 1,
+ },
+ Stonecutter {
+ input: 1,
+ result: 1,
+ },
+}
diff --git a/azalea-inventory/src/operations.rs b/azalea-inventory/src/operations.rs
new file mode 100644
index 00000000..1379b8a9
--- /dev/null
+++ b/azalea-inventory/src/operations.rs
@@ -0,0 +1,698 @@
+use std::ops::RangeInclusive;
+
+use azalea_buf::McBuf;
+
+use crate::{
+ item::MaxStackSizeExt, AnvilMenuLocation, BeaconMenuLocation, BlastFurnaceMenuLocation,
+ BrewingStandMenuLocation, CartographyTableMenuLocation, CraftingMenuLocation,
+ EnchantmentMenuLocation, FurnaceMenuLocation, Generic3x3MenuLocation, Generic9x1MenuLocation,
+ Generic9x2MenuLocation, Generic9x3MenuLocation, Generic9x4MenuLocation, Generic9x5MenuLocation,
+ Generic9x6MenuLocation, GrindstoneMenuLocation, HopperMenuLocation, ItemSlot, ItemSlotData,
+ LecternMenuLocation, LegacySmithingMenuLocation, LoomMenuLocation, Menu, MenuLocation,
+ MerchantMenuLocation, Player, PlayerMenuLocation, ShulkerBoxMenuLocation, SmithingMenuLocation,
+ SmokerMenuLocation, StonecutterMenuLocation,
+};
+
+#[derive(Debug, Clone)]
+pub enum ClickOperation {
+ Pickup(PickupClick),
+ QuickMove(QuickMoveClick),
+ Swap(SwapClick),
+ Clone(CloneClick),
+ Throw(ThrowClick),
+ QuickCraft(QuickCraftClick),
+ PickupAll(PickupAllClick),
+}
+
+#[derive(Debug, Clone)]
+pub enum PickupClick {
+ /// Left mouse click. Note that in the protocol, None is represented as
+ /// -999.
+ Left { slot: Option<u16> },
+ /// Right mouse click. Note that in the protocol, None is represented as
+ /// -999.
+ Right { slot: Option<u16> },
+ /// Drop cursor stack.
+ LeftOutside,
+ /// Drop cursor single item.
+ RightOutside,
+}
+impl From<PickupClick> for ClickOperation {
+ fn from(click: PickupClick) -> Self {
+ ClickOperation::Pickup(click)
+ }
+}
+
+/// Shift click
+#[derive(Debug, Clone)]
+pub enum QuickMoveClick {
+ /// Shift + left mouse click
+ Left { slot: u16 },
+ /// Shift + right mouse click (identical behavior)
+ Right { slot: u16 },
+}
+impl From<QuickMoveClick> for ClickOperation {
+ fn from(click: QuickMoveClick) -> Self {
+ ClickOperation::QuickMove(click)
+ }
+}
+
+/// Used when you press number keys or F in an inventory.
+#[derive(Debug, Clone)]
+pub struct SwapClick {
+ pub source_slot: u16,
+ pub target_slot: u8,
+}
+
+impl From<SwapClick> for ClickOperation {
+ fn from(click: SwapClick) -> Self {
+ ClickOperation::Swap(click)
+ }
+}
+/// Middle click, only defined for creative players in non-player
+/// inventories.
+#[derive(Debug, Clone)]
+pub struct CloneClick {
+ pub slot: u16,
+}
+impl From<CloneClick> for ClickOperation {
+ fn from(click: CloneClick) -> Self {
+ ClickOperation::Clone(click)
+ }
+}
+#[derive(Debug, Clone)]
+pub enum ThrowClick {
+ /// Drop key (Q)
+ Single { slot: u16 },
+ /// Ctrl + drop key (Q)
+ All { slot: u16 },
+}
+impl From<ThrowClick> for ClickOperation {
+ fn from(click: ThrowClick) -> Self {
+ ClickOperation::Throw(click)
+ }
+}
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub struct QuickCraftClick {
+ pub kind: QuickCraftKind,
+ pub status: QuickCraftStatus,
+}
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub enum QuickCraftKind {
+ Left,
+ Right,
+ Middle,
+}
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub enum QuickCraftStatusKind {
+ /// Starting drag
+ Start,
+ /// Add slot
+ Add,
+ /// Ending drag
+ End,
+}
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub enum QuickCraftStatus {
+ /// Starting drag
+ Start,
+ /// Add a slot.
+ Add { slot: u16 },
+ /// Ending drag
+ End,
+}
+impl From<QuickCraftStatus> for QuickCraftStatusKind {
+ fn from(status: QuickCraftStatus) -> Self {
+ match status {
+ QuickCraftStatus::Start => QuickCraftStatusKind::Start,
+ QuickCraftStatus::Add { .. } => QuickCraftStatusKind::Add,
+ QuickCraftStatus::End => QuickCraftStatusKind::End,
+ }
+ }
+}
+
+/// Double click
+#[derive(Debug, Clone)]
+pub struct PickupAllClick {
+ /// The slot that we're double clicking on. It should be empty or at least
+ /// not pickup-able (since the carried item is used as the filter).
+ pub slot: u16,
+ /// Impossible in vanilla clients.
+ pub reversed: bool,
+}
+impl From<PickupAllClick> for ClickOperation {
+ fn from(click: PickupAllClick) -> Self {
+ ClickOperation::PickupAll(click)
+ }
+}
+
+impl ClickOperation {
+ /// Return the slot number that this operation is acting on, if any.
+ ///
+ /// Note that in the protocol, "None" is represented as -999.
+ pub fn slot_num(&self) -> Option<u16> {
+ match self {
+ ClickOperation::Pickup(pickup) => match pickup {
+ PickupClick::Left { slot } => *slot,
+ PickupClick::Right { slot } => *slot,
+ PickupClick::LeftOutside => None,
+ PickupClick::RightOutside => None,
+ },
+ ClickOperation::QuickMove(quick_move) => match quick_move {
+ QuickMoveClick::Left { slot } => Some(*slot),
+ QuickMoveClick::Right { slot } => Some(*slot),
+ },
+ ClickOperation::Swap(swap) => Some(swap.source_slot),
+ ClickOperation::Clone(clone) => Some(clone.slot),
+ ClickOperation::Throw(throw) => match throw {
+ ThrowClick::Single { slot } => Some(*slot),
+ ThrowClick::All { slot } => Some(*slot),
+ },
+ ClickOperation::QuickCraft(quick_craft) => match quick_craft.status {
+ QuickCraftStatus::Start => None,
+ QuickCraftStatus::Add { slot } => Some(slot),
+ QuickCraftStatus::End => None,
+ },
+ ClickOperation::PickupAll(pickup_all) => Some(pickup_all.slot),
+ }
+ }
+
+ pub fn button_num(&self) -> u8 {
+ match self {
+ ClickOperation::Pickup(pickup) => match pickup {
+ PickupClick::Left { .. } => 0,
+ PickupClick::Right { .. } => 1,
+ PickupClick::LeftOutside => 0,
+ PickupClick::RightOutside => 1,
+ },
+ ClickOperation::QuickMove(quick_move) => match quick_move {
+ QuickMoveClick::Left { .. } => 0,
+ QuickMoveClick::Right { .. } => 1,
+ },
+ ClickOperation::Swap(swap) => swap.target_slot,
+ ClickOperation::Clone(_) => 2,
+ ClickOperation::Throw(throw) => match throw {
+ ThrowClick::Single { .. } => 0,
+ ThrowClick::All { .. } => 1,
+ },
+ ClickOperation::QuickCraft(quick_craft) => match quick_craft {
+ QuickCraftClick {
+ kind: QuickCraftKind::Left,
+ status: QuickCraftStatus::Start,
+ } => 0,
+ QuickCraftClick {
+ kind: QuickCraftKind::Right,
+ status: QuickCraftStatus::Start,
+ } => 4,
+ QuickCraftClick {
+ kind: QuickCraftKind::Middle,
+ status: QuickCraftStatus::Start,
+ } => 8,
+ QuickCraftClick {
+ kind: QuickCraftKind::Left,
+ status: QuickCraftStatus::Add { .. },
+ } => 1,
+ QuickCraftClick {
+ kind: QuickCraftKind::Right,
+ status: QuickCraftStatus::Add { .. },
+ } => 5,
+ QuickCraftClick {
+ kind: QuickCraftKind::Middle,
+ status: QuickCraftStatus::Add { .. },
+ } => 9,
+ QuickCraftClick {
+ kind: QuickCraftKind::Left,
+ status: QuickCraftStatus::End,
+ } => 2,
+ QuickCraftClick {
+ kind: QuickCraftKind::Right,
+ status: QuickCraftStatus::End,
+ } => 6,
+ QuickCraftClick {
+ kind: QuickCraftKind::Middle,
+ status: QuickCraftStatus::End,
+ } => 10,
+ },
+ ClickOperation::PickupAll(_) => 0,
+ }
+ }
+
+ pub fn click_type(&self) -> ClickType {
+ match self {
+ ClickOperation::Pickup(_) => ClickType::Pickup,
+ ClickOperation::QuickMove(_) => ClickType::QuickMove,
+ ClickOperation::Swap(_) => ClickType::Swap,
+ ClickOperation::Clone(_) => ClickType::Clone,
+ ClickOperation::Throw(_) => ClickType::Throw,
+ ClickOperation::QuickCraft(_) => ClickType::QuickCraft,
+ ClickOperation::PickupAll(_) => ClickType::PickupAll,
+ }
+ }
+}
+
+#[derive(McBuf, Clone, Copy, Debug)]
+pub enum ClickType {
+ Pickup = 0,
+ QuickMove = 1,
+ Swap = 2,
+ Clone = 3,
+ Throw = 4,
+ QuickCraft = 5,
+ PickupAll = 6,
+}
+
+impl Menu {
+ /// Shift-click a slot in this menu.
+ pub fn quick_move_stack(&mut self, slot_index: usize) -> ItemSlot {
+ let slot = self.slot(slot_index);
+ if slot.is_none() {
+ return ItemSlot::Empty;
+ };
+
+ let slot_location = self
+ .location_for_slot(slot_index)
+ .expect("we just checked to make sure the slot is Some above, so this shouldn't be able to error");
+ match slot_location {
+ MenuLocation::Player(l) => match l {
+ PlayerMenuLocation::CraftResult => {
+ self.try_move_item_to_slots(slot_index, Player::INVENTORY_SLOTS);
+ }
+ PlayerMenuLocation::Craft => {
+ self.try_move_item_to_slots(slot_index, Player::INVENTORY_SLOTS);
+ }
+ PlayerMenuLocation::Armor => {
+ self.try_move_item_to_slots(slot_index, Player::INVENTORY_SLOTS);
+ }
+ _ => {
+ // TODO: armor handling (see quickMoveStack in
+ // InventoryMenu.java)
+
+ // if slot.kind().is_armor() &&
+
+ // also offhand handling
+
+ if l == PlayerMenuLocation::Inventory {
+ // shift-clicking in hotbar moves to inventory, and vice versa
+ if Player::is_hotbar_slot(slot_index) {
+ self.try_move_item_to_slots(
+ slot_index,
+ Player::INVENTORY_WITHOUT_HOTBAR_SLOTS,
+ );
+ } else {
+ self.try_move_item_to_slots(slot_index, Player::HOTBAR_SLOTS);
+ }
+ } else {
+ self.try_move_item_to_slots(slot_index, self.player_slots_range());
+ }
+ }
+ },
+ MenuLocation::Generic9x1(l) => match l {
+ Generic9x1MenuLocation::Contents => {
+ self.try_move_item_to_slots(slot_index, self.player_slots_range());
+ }
+ Generic9x1MenuLocation::Player => {
+ self.try_move_item_to_slots_or_toggle_hotbar(
+ slot_index,
+ Menu::GENERIC9X1_CONTENTS_SLOTS,
+ );
+ }
+ },
+ MenuLocation::Generic9x2(l) => match l {
+ Generic9x2MenuLocation::Contents => {
+ self.try_move_item_to_slots(slot_index, self.player_slots_range());
+ }
+ Generic9x2MenuLocation::Player => {
+ self.try_move_item_to_slots_or_toggle_hotbar(
+ slot_index,
+ Menu::GENERIC9X2_CONTENTS_SLOTS,
+ );
+ }
+ },
+ MenuLocation::Generic9x3(l) => match l {
+ Generic9x3MenuLocation::Contents => {
+ self.try_move_item_to_slots(slot_index, self.player_slots_range());
+ }
+ Generic9x3MenuLocation::Player => {
+ self.try_move_item_to_slots_or_toggle_hotbar(
+ slot_index,
+ Menu::GENERIC9X3_CONTENTS_SLOTS,
+ );
+ }
+ },
+ MenuLocation::Generic9x4(l) => match l {
+ Generic9x4MenuLocation::Contents => {
+ self.try_move_item_to_slots(slot_index, self.player_slots_range());
+ }
+ Generic9x4MenuLocation::Player => {
+ self.try_move_item_to_slots_or_toggle_hotbar(
+ slot_index,
+ Menu::GENERIC9X4_CONTENTS_SLOTS,
+ );
+ }
+ },
+ MenuLocation::Generic9x5(l) => match l {
+ Generic9x5MenuLocation::Contents => {
+ self.try_move_item_to_slots(slot_index, self.player_slots_range());
+ }
+ Generic9x5MenuLocation::Player => {
+ self.try_move_item_to_slots_or_toggle_hotbar(
+ slot_index,
+ Menu::GENERIC9X5_CONTENTS_SLOTS,
+ );
+ }
+ },
+ MenuLocation::Generic9x6(l) => match l {
+ Generic9x6MenuLocation::Contents => {
+ self.try_move_item_to_slots(slot_index, self.player_slots_range());
+ }
+ Generic9x6MenuLocation::Player => {
+ self.try_move_item_to_slots_or_toggle_hotbar(
+ slot_index,
+ Menu::GENERIC9X6_CONTENTS_SLOTS,
+ );
+ }
+ },
+ MenuLocation::Generic3x3(l) => match l {
+ Generic3x3MenuLocation::Contents => {
+ self.try_move_item_to_slots(slot_index, self.player_slots_range());
+ }
+ Generic3x3MenuLocation::Player => {
+ self.try_move_item_to_slots_or_toggle_hotbar(
+ slot_index,
+ Menu::GENERIC3X3_CONTENTS_SLOTS,
+ );
+ }
+ },
+ MenuLocation::Anvil(l) => match l {
+ AnvilMenuLocation::Player => {
+ self.try_move_item_to_slots_or_toggle_hotbar(
+ slot_index,
+ Menu::ANVIL_FIRST_SLOT..=Menu::ANVIL_SECOND_SLOT,
+ );
+ }
+ _ => {
+ self.try_move_item_to_slots(slot_index, self.player_slots_range());
+ }
+ },
+ MenuLocation::Beacon(l) => match l {
+ BeaconMenuLocation::Payment => {
+ self.try_move_item_to_slots(slot_index, self.player_slots_range());
+ }
+ BeaconMenuLocation::Player => {
+ self.try_move_item_to_slots(
+ slot_index,
+ Menu::BEACON_PAYMENT_SLOT..=Menu::BEACON_PAYMENT_SLOT,
+ );
+ }
+ },
+ MenuLocation::BlastFurnace(l) => match l {
+ BlastFurnaceMenuLocation::Player => {
+ self.try_move_item_to_slots(
+ slot_index,
+ Menu::BLAST_FURNACE_INGREDIENT_SLOT..=Menu::BLAST_FURNACE_FUEL_SLOT,
+ );
+ }
+ _ => {
+ self.try_move_item_to_slots(slot_index, self.player_slots_range());
+ }
+ },
+ MenuLocation::BrewingStand(l) => match l {
+ BrewingStandMenuLocation::Player => {
+ self.try_move_item_to_slots(
+ slot_index,
+ *Menu::BREWING_STAND_BOTTLES_SLOTS.start()
+ ..=Menu::BREWING_STAND_INGREDIENT_SLOT,
+ );
+ }
+ _ => {
+ self.try_move_item_to_slots(slot_index, self.player_slots_range());
+ }
+ },
+ MenuLocation::Crafting(l) => match l {
+ CraftingMenuLocation::Player => {
+ self.try_move_item_to_slots_or_toggle_hotbar(
+ slot_index,
+ Menu::CRAFTING_GRID_SLOTS,
+ );
+ }
+ _ => {
+ self.try_move_item_to_slots(slot_index, self.player_slots_range());
+ }
+ },
+ MenuLocation::Enchantment(l) => match l {
+ EnchantmentMenuLocation::Player => {
+ self.try_move_item_to_slots_or_toggle_hotbar(
+ slot_index,
+ Menu::ENCHANTMENT_ITEM_SLOT..=Menu::ENCHANTMENT_LAPIS_SLOT,
+ );
+ }
+ _ => {
+ self.try_move_item_to_slots(slot_index, self.player_slots_range());
+ }
+ },
+ MenuLocation::Furnace(l) => match l {
+ FurnaceMenuLocation::Player => {
+ self.try_move_item_to_slots(
+ slot_index,
+ Menu::FURNACE_INGREDIENT_SLOT..=Menu::FURNACE_FUEL_SLOT,
+ );
+ }
+ _ => {
+ self.try_move_item_to_slots(slot_index, self.player_slots_range());
+ }
+ },
+ MenuLocation::Grindstone(l) => match l {
+ GrindstoneMenuLocation::Player => {
+ self.try_move_item_to_slots_or_toggle_hotbar(
+ slot_index,
+ Menu::GRINDSTONE_INPUT_SLOT..=Menu::GRINDSTONE_ADDITIONAL_SLOT,
+ );
+ }
+ _ => {
+ self.try_move_item_to_slots(slot_index, self.player_slots_range());
+ }
+ },
+ MenuLocation::Hopper(l) => match l {
+ HopperMenuLocation::Player => {
+ self.try_move_item_to_slots_or_toggle_hotbar(
+ slot_index,
+ Menu::HOPPER_CONTENTS_SLOTS,
+ );
+ }
+ _ => {
+ self.try_move_item_to_slots(slot_index, self.player_slots_range());
+ }
+ },
+ MenuLocation::Lectern(l) => match l {
+ LecternMenuLocation::Player => {
+ self.try_move_item_to_slots_or_toggle_hotbar(
+ slot_index,
+ Menu::LECTERN_BOOK_SLOT..=Menu::LECTERN_BOOK_SLOT,
+ );
+ }
+ _ => {
+ self.try_move_item_to_slots(slot_index, self.player_slots_range());
+ }
+ },
+ MenuLocation::Loom(l) => match l {
+ LoomMenuLocation::Player => {
+ self.try_move_item_to_slots_or_toggle_hotbar(
+ slot_index,
+ Menu::LOOM_BANNER_SLOT..=Menu::LOOM_PATTERN_SLOT,
+ );
+ }
+ _ => {
+ self.try_move_item_to_slots(slot_index, self.player_slots_range());
+ }
+ },
+ MenuLocation::Merchant(l) => match l {
+ MerchantMenuLocation::Player => {
+ self.try_move_item_to_slots_or_toggle_hotbar(
+ slot_index,
+ Menu::MERCHANT_PAYMENTS_SLOTS,
+ );
+ }
+ _ => {
+ self.try_move_item_to_slots(slot_index, self.player_slots_range());
+ }
+ },
+ MenuLocation::ShulkerBox(l) => match l {
+ ShulkerBoxMenuLocation::Player => {
+ self.try_move_item_to_slots_or_toggle_hotbar(
+ slot_index,
+ Menu::SHULKER_BOX_CONTENTS_SLOTS,
+ );
+ }
+ _ => {
+ self.try_move_item_to_slots(slot_index, self.player_slots_range());
+ }
+ },
+ MenuLocation::LegacySmithing(l) => match l {
+ LegacySmithingMenuLocation::Player => {
+ self.try_move_item_to_slots_or_toggle_hotbar(
+ slot_index,
+ Menu::LEGACY_SMITHING_INPUT_SLOT..=Menu::LEGACY_SMITHING_ADDITIONAL_SLOT,
+ );
+ }
+ _ => {
+ self.try_move_item_to_slots(slot_index, self.player_slots_range());
+ }
+ },
+ MenuLocation::Smithing(l) => match l {
+ SmithingMenuLocation::Player => {
+ self.try_move_item_to_slots_or_toggle_hotbar(
+ slot_index,
+ Menu::SMITHING_TEMPLATE_SLOT..=Menu::SMITHING_ADDITIONAL_SLOT,
+ );
+ }
+ _ => {
+ self.try_move_item_to_slots(slot_index, self.player_slots_range());
+ }
+ },
+ MenuLocation::Smoker(l) => match l {
+ SmokerMenuLocation::Player => {
+ self.try_move_item_to_slots(
+ slot_index,
+ Menu::SMOKER_INGREDIENT_SLOT..=Menu::SMOKER_FUEL_SLOT,
+ );
+ }
+ _ => {
+ self.try_move_item_to_slots(slot_index, self.player_slots_range());
+ }
+ },
+ MenuLocation::CartographyTable(l) => match l {
+ CartographyTableMenuLocation::Player => {
+ self.try_move_item_to_slots_or_toggle_hotbar(
+ slot_index,
+ Menu::CARTOGRAPHY_TABLE_MAP_SLOT..=Menu::CARTOGRAPHY_TABLE_ADDITIONAL_SLOT,
+ );
+ }
+ _ => {
+ self.try_move_item_to_slots(slot_index, self.player_slots_range());
+ }
+ },
+ MenuLocation::Stonecutter(l) => match l {
+ StonecutterMenuLocation::Player => {
+ self.try_move_item_to_slots_or_toggle_hotbar(
+ slot_index,
+ Menu::STONECUTTER_INPUT_SLOT..=Menu::STONECUTTER_INPUT_SLOT,
+ );
+ }
+ _ => {
+ self.try_move_item_to_slots(slot_index, self.player_slots_range());
+ }
+ },
+ }
+
+ ItemSlot::Empty
+ }
+
+ fn try_move_item_to_slots_or_toggle_hotbar(
+ &mut self,
+ slot_index: usize,
+ target_slot_indexes: RangeInclusive<usize>,
+ ) {
+ if !self.try_move_item_to_slots(slot_index, target_slot_indexes) {
+ self.try_move_item_to_slots(
+ slot_index,
+ if self.is_hotbar_slot(slot_index) {
+ self.player_slots_without_hotbar_range()
+ } else {
+ self.hotbar_slots_range()
+ },
+ );
+ }
+ }
+
+ /// Whether the given item could be placed in this menu.
+ ///
+ /// TODO: right now this always returns true
+ pub fn may_place(&self, _target_slot_index: usize, _item: &ItemSlotData) -> bool {
+ true
+ }
+
+ /// Whether the item in the given slot could be clicked and picked up.
+ /// TODO: right now this always returns true
+ pub fn may_pickup(&self, _source_slot_index: usize) -> bool {
+ true
+ }
+
+ /// Get the maximum number of items that can be placed in this slot.
+ pub fn max_stack_size(&self, _target_slot_index: usize) -> u8 {
+ 64
+ }
+
+ /// Try moving an item to a set of slots in this menu.
+ ///
+ /// Returns the updated item slot.
+ fn try_move_item_to_slots(
+ &mut self,
+ item_slot_index: usize,
+ target_slot_indexes: RangeInclusive<usize>,
+ ) -> bool {
+ let mut item_slot = self.slot(item_slot_index).unwrap().clone();
+
+ // first see if we can stack it with another item
+ if item_slot.kind().stackable() {
+ for target_slot_index in target_slot_indexes.clone() {
+ self.move_item_to_slot_if_stackable(&mut item_slot, target_slot_index);
+ if item_slot.is_empty() {
+ break;
+ }
+ }
+ }
+
+ // and if not then just try putting it in an empty slot
+ if item_slot.is_present() {
+ for target_slot_index in target_slot_indexes {
+ self.move_item_to_slot_if_empty(&mut item_slot, target_slot_index);
+ if item_slot.is_empty() {
+ break;
+ }
+ }
+ }
+
+ item_slot.is_empty()
+ }
+
+ /// Merge this item slot into the target item slot, only if the target item
+ /// slot is present and the same item.
+ fn move_item_to_slot_if_stackable(
+ &mut self,
+ item_slot: &mut ItemSlot,
+ target_slot_index: usize,
+ ) {
+ let ItemSlot::Present(item) = item_slot else {
+ return;
+ };
+ let target_slot = self.slot(target_slot_index).unwrap();
+ if let ItemSlot::Present(target_item) = target_slot {
+ // the target slot is empty, so we can just move the item there
+ if self.may_place(target_slot_index, item) && target_item.is_same_item_and_nbt(item) {
+ let slot_item_limit = self.max_stack_size(target_slot_index);
+ let new_target_slot_data = item.split(u8::min(slot_item_limit, item.count as u8));
+
+ // get the target slot again but mut this time so we can update it
+ let target_slot = self.slot_mut(target_slot_index).unwrap();
+ *target_slot = ItemSlot::Present(new_target_slot_data);
+
+ item_slot.update_empty();
+ }
+ }
+ }
+
+ fn move_item_to_slot_if_empty(&mut self, item_slot: &mut ItemSlot, target_slot_index: usize) {
+ let ItemSlot::Present(item) = item_slot else {
+ return;
+ };
+ let target_slot = self.slot(target_slot_index).unwrap();
+ if target_slot.is_empty() && self.may_place(target_slot_index, item) {
+ let slot_item_limit = self.max_stack_size(target_slot_index);
+ let new_target_slot_data = item.split(u8::min(slot_item_limit, item.count as u8));
+
+ let target_slot = self.slot_mut(target_slot_index).unwrap();
+ *target_slot = ItemSlot::Present(new_target_slot_data);
+ item_slot.update_empty();
+ }
+ }
+}
diff --git a/azalea-inventory/src/slot.rs b/azalea-inventory/src/slot.rs
new file mode 100644
index 00000000..cef555d7
--- /dev/null
+++ b/azalea-inventory/src/slot.rs
@@ -0,0 +1,146 @@
+use azalea_buf::{BufReadError, McBuf, McBufReadable, McBufWritable};
+use azalea_nbt::Nbt;
+use std::io::{Cursor, Write};
+
+/// Either an item in an inventory or nothing.
+#[derive(Debug, Clone, Default, PartialEq)]
+pub enum ItemSlot {
+ #[default]
+ Empty,
+ Present(ItemSlotData),
+}
+
+impl ItemSlot {
+ /// Check if the slot is ItemSlot::Empty, if the count is <= 0, or if the
+ /// item is air.
+ ///
+ /// This is the opposite of [`ItemSlot::is_present`].
+ pub fn is_empty(&self) -> bool {
+ match self {
+ ItemSlot::Empty => true,
+ ItemSlot::Present(item) => item.is_empty(),
+ }
+ }
+ /// Check if the slot is not ItemSlot::Empty, if the count is > 0, and if
+ /// the item is not air.
+ ///
+ /// This is the opposite of [`ItemSlot::is_empty`].
+ pub fn is_present(&self) -> bool {
+ !self.is_empty()
+ }
+
+ /// Return the amount of the item in the slot, or 0 if the slot is empty.
+ ///
+ /// Note that it's possible for the count to be zero or negative when the
+ /// slot is present.
+ pub fn count(&self) -> i8 {
+ match self {
+ ItemSlot::Empty => 0,
+ ItemSlot::Present(i) => i.count,
+ }
+ }
+
+ /// Remove `count` items from this slot, returning the removed items.
+ pub fn split(&mut self, count: u8) -> ItemSlot {
+ match self {
+ ItemSlot::Empty => ItemSlot::Empty,
+ ItemSlot::Present(i) => {
+ let returning = i.split(count);
+ if i.is_empty() {
+ *self = ItemSlot::Empty;
+ }
+ ItemSlot::Present(returning)
+ }
+ }
+ }
+
+ /// Get the `kind` of the item in this slot, or
+ /// [`azalea_registry::Item::Air`]
+ pub fn kind(&self) -> azalea_registry::Item {
+ match self {
+ ItemSlot::Empty => azalea_registry::Item::Air,
+ ItemSlot::Present(i) => i.kind,
+ }
+ }
+
+ /// Update whether this slot is empty, based on the count.
+ pub fn update_empty(&mut self) {
+ if let ItemSlot::Present(i) = self {
+ if i.is_empty() {
+ *self = ItemSlot::Empty;
+ }
+ }
+ }
+}
+
+/// An item in an inventory, with a count and NBT. Usually you want [`ItemSlot`]
+/// or [`azalea_registry::Item`] instead.
+#[derive(Debug, Clone, McBuf, PartialEq)]
+pub struct ItemSlotData {
+ pub kind: azalea_registry::Item,
+ /// The amount of the item in this slot.
+ ///
+ /// The count can be zero or negative, but this is rare.
+ pub count: i8,
+ pub nbt: Nbt,
+}
+
+impl ItemSlotData {
+ /// Remove `count` items from this slot, returning the removed items.
+ pub fn split(&mut self, count: u8) -> ItemSlotData {
+ let returning_count = i8::min(count as i8, self.count);
+ let mut returning = self.clone();
+ returning.count = returning_count;
+ self.count -= returning_count;
+ returning
+ }
+
+ /// Check if the count of the item is <= 0 or if the item is air.
+ pub fn is_empty(&self) -> bool {
+ self.count <= 0 || self.kind == azalea_registry::Item::Air
+ }
+
+ /// Whether this item is the same as another item, ignoring the count.
+ ///
+ /// ```
+ /// # use azalea_inventory::ItemSlotData;
+ /// # use azalea_registry::Item;
+ /// let mut a = ItemSlotData {
+ /// kind: Item::Stone,
+ /// count: 1,
+ /// nbt: Default::default(),
+ /// };
+ /// let mut b = ItemSlotData {
+ /// kind: Item::Stone,
+ /// count: 2,
+ /// nbt: Default::default(),
+ /// };
+ /// assert!(a.is_same_item_and_nbt(&b));
+ ///
+ /// b.kind = Item::Dirt;
+ /// assert!(!a.is_same_item_and_nbt(&b));
+ /// ```
+ pub fn is_same_item_and_nbt(&self, other: &ItemSlotData) -> bool {
+ self.kind == other.kind && self.nbt == other.nbt
+ }
+}
+
+impl McBufReadable for ItemSlot {
+ fn read_from(buf: &mut Cursor<&[u8]>) -> Result<Self, BufReadError> {
+ let slot = Option::<ItemSlotData>::read_from(buf)?;
+ Ok(slot.map_or(ItemSlot::Empty, ItemSlot::Present))
+ }
+}
+
+impl McBufWritable for ItemSlot {
+ fn write_into(&self, buf: &mut impl Write) -> Result<(), std::io::Error> {
+ match self {
+ ItemSlot::Empty => false.write_into(buf)?,
+ ItemSlot::Present(i) => {
+ true.write_into(buf)?;
+ i.write_into(buf)?;
+ }
+ };
+ Ok(())
+ }
+}
diff --git a/azalea-nbt/benches/compare.rs b/azalea-nbt/benches/compare.rs
index 139a8b65..1634b45b 100755
--- a/azalea-nbt/benches/compare.rs
+++ b/azalea-nbt/benches/compare.rs
@@ -22,21 +22,21 @@ pub fn bench_read_file(filename: &str, c: &mut Criterion) {
let mut group = c.benchmark_group(filename);
group.throughput(Throughput::Bytes(input.len() as u64));
- // group.bench_function("azalea_parse", |b| {
- // b.iter(|| {
- // let input = black_box(input);
- // let nbt = azalea_nbt::Nbt::read(&mut Cursor::new(&input)).unwrap();
- // black_box(nbt);
- // })
- // });
+ group.bench_function("azalea_parse", |b| {
+ b.iter(|| {
+ let input = black_box(input);
+ let nbt = azalea_nbt::Nbt::read(&mut Cursor::new(&input)).unwrap();
+ black_box(nbt);
+ })
+ });
- // group.bench_function("graphite_parse", |b| {
- // b.iter(|| {
- // let input = black_box(input);
- // let nbt = graphite_binary::nbt::decode::read(&mut
- // &input[..]).unwrap(); black_box(nbt);
- // })
- // });
+ group.bench_function("graphite_parse", |b| {
+ b.iter(|| {
+ let input = black_box(input);
+ let nbt = graphite_binary::nbt::decode::read(&mut &input[..]).unwrap();
+ black_box(nbt);
+ })
+ });
// group.bench_function("valence_parse", |b| {
// b.iter(|| {
diff --git a/azalea-physics/Cargo.toml b/azalea-physics/Cargo.toml
index dd579471..ca7390eb 100644
--- a/azalea-physics/Cargo.toml
+++ b/azalea-physics/Cargo.toml
@@ -11,6 +11,7 @@ version = "0.6.0"
[dependencies]
azalea-block = { path = "../azalea-block", version = "^0.6.0" }
azalea-core = { path = "../azalea-core", version = "^0.6.0" }
+azalea-inventory = { version = "0.1.0", path = "../azalea-inventory" }
azalea-registry = { path = "../azalea-registry", version = "^0.6.0" }
azalea-world = { path = "../azalea-world", version = "^0.6.0" }
bevy_app = "0.10.0"
diff --git a/azalea-physics/src/clip.rs b/azalea-physics/src/clip.rs
new file mode 100644
index 00000000..ca85c32a
--- /dev/null
+++ b/azalea-physics/src/clip.rs
@@ -0,0 +1,232 @@
+use azalea_block::BlockState;
+use azalea_core::{lerp, BlockHitResult, BlockPos, Direction, Vec3, EPSILON};
+use azalea_inventory::ItemSlot;
+use azalea_world::ChunkStorage;
+use bevy_ecs::entity::Entity;
+
+use crate::collision::{BlockWithShape, VoxelShape};
+
+#[derive(Debug, Clone)]
+pub struct ClipContext {
+ pub from: Vec3,
+ pub to: Vec3,
+ pub block_shape_type: BlockShapeType,
+ pub fluid_pick_type: FluidPickType,
+ // pub collision_context: EntityCollisionContext,
+}
+impl ClipContext {
+ // minecraft passes in the world and blockpos here... but it doesn't actually
+ // seem necessary?
+ pub fn block_shape(&self, block_state: BlockState) -> &VoxelShape {
+ // TODO: implement the other shape getters
+ // (see the ClipContext.Block class in the vanilla source)
+ match self.block_shape_type {
+ BlockShapeType::Collider => block_state.shape(),
+ BlockShapeType::Outline => block_state.shape(),
+ BlockShapeType::Visual => block_state.shape(),
+ BlockShapeType::FallDamageResetting => block_state.shape(),
+ }
+ }
+}
+
+#[derive(Debug, Copy, Clone)]
+pub enum BlockShapeType {
+ Collider,
+ Outline,
+ Visual,
+ FallDamageResetting,
+}
+#[derive(Debug, Copy, Clone)]
+pub enum FluidPickType {
+ None,
+ SourceOnly,
+ Any,
+ Water,
+}
+#[derive(Debug, Clone)]
+pub struct EntityCollisionContext {
+ pub descending: bool,
+ pub entity_bottom: f64,
+ pub held_item: ItemSlot,
+ // pub can_stand_on_fluid: Box<dyn Fn(&FluidState) -> bool>,
+ pub entity: Entity,
+}
+
+pub fn clip(chunk_storage: &ChunkStorage, context: ClipContext) -> BlockHitResult {
+ traverse_blocks(
+ context.from,
+ context.to,
+ context,
+ |context, block_pos| {
+ let block_state = chunk_storage.get_block_state(block_pos).unwrap_or_default();
+ // TODO: add fluid stuff to this (see getFluidState in vanilla source)
+ let block_shape = context.block_shape(block_state);
+ clip_with_interaction_override(
+ &context.from,
+ &context.to,
+ block_pos,
+ block_shape,
+ &block_state,
+ )
+ // let block_distance = if let Some(block_hit_result) =
+ // block_hit_result { context.from.distance_to_sqr(&
+ // block_hit_result.location) } else {
+ // f64::MAX
+ // };
+ },
+ |context| {
+ let vec = context.from - context.to;
+ BlockHitResult::miss(
+ context.to,
+ Direction::nearest(vec),
+ BlockPos::from(context.to),
+ )
+ },
+ )
+}
+
+// default BlockHitResult clipWithInteractionOverride(Vec3 world, Vec3 from,
+// BlockPos to, VoxelShape shape, BlockState block) {
+// BlockHitResult blockHitResult = shape.clip(world, from, to);
+// if (blockHitResult != null) {
+// BlockHitResult var7 = block.getInteractionShape(this, to).clip(world,
+// from, to); if (var7 != null
+// && var7.getLocation().subtract(world).lengthSqr() <
+// blockHitResult.getLocation().subtract(world).lengthSqr()) { return
+// blockHitResult.withDirection(var7.getDirection()); }
+// }
+
+// return blockHitResult;
+// }
+fn clip_with_interaction_override(
+ from: &Vec3,
+ to: &Vec3,
+ block_pos: &BlockPos,
+ block_shape: &VoxelShape,
+ block_state: &BlockState,
+) -> Option<BlockHitResult> {
+ let block_hit_result = block_shape.clip(from, to, block_pos);
+ if let Some(block_hit_result) = block_hit_result {
+ // TODO: minecraft calls .getInteractionShape here
+ // are there even any blocks that have a physics shape different from the
+ // interaction shape???
+ // (if not then you can delete this comment)
+ // (if there are then you have to implement BlockState::interaction_shape, lol
+ // have fun)
+ let interaction_shape = block_state.shape();
+ let interaction_hit_result = interaction_shape.clip(from, to, block_pos);
+ if let Some(interaction_hit_result) = interaction_hit_result {
+ if interaction_hit_result.location.distance_to_sqr(from)
+ < block_hit_result.location.distance_to_sqr(from)
+ {
+ return Some(block_hit_result.with_direction(interaction_hit_result.direction));
+ }
+ }
+ Some(block_hit_result)
+ } else {
+ block_hit_result
+ }
+}
+
+pub fn traverse_blocks<C, T>(
+ from: Vec3,
+ to: Vec3,
+ context: C,
+ get_hit_result: impl Fn(&C, &BlockPos) -> Option<T>,
+ get_miss_result: impl Fn(&C) -> T,
+) -> T {
+ if from == to {
+ return get_miss_result(&context);
+ }
+
+ let right_after_end = Vec3 {
+ x: lerp(-EPSILON, to.x, from.x),
+ y: lerp(-EPSILON, to.y, from.y),
+ z: lerp(-EPSILON, to.z, from.z),
+ };
+
+ let right_before_start = Vec3 {
+ x: lerp(-EPSILON, from.x, to.x),
+ y: lerp(-EPSILON, from.y, to.y),
+ z: lerp(-EPSILON, from.z, to.z),
+ };
+
+ let mut current_block = BlockPos::from(right_before_start);
+ if let Some(data) = get_hit_result(&context, &current_block) {
+ return data;
+ }
+
+ let vec = right_after_end - right_before_start;
+
+ /// Returns either -1, 0, or 1, depending on whether the number is negative,
+ /// zero, or positive.
+ ///
+ /// This function exists because f64::signum doesn't check for 0.
+ fn get_number_sign(num: f64) -> f64 {
+ if num == 0. {
+ 0.
+ } else {
+ num.signum()
+ }
+ }
+
+ let vec_sign = Vec3 {
+ x: get_number_sign(vec.x),
+ y: get_number_sign(vec.y),
+ z: get_number_sign(vec.z),
+ };
+
+ #[rustfmt::skip]
+ let percentage_step = Vec3 {
+ x: if vec_sign.x == 0. { f64::MAX } else { vec_sign.x / vec.x },
+ y: if vec_sign.y == 0. { f64::MAX } else { vec_sign.y / vec.y },
+ z: if vec_sign.z == 0. { f64::MAX } else { vec_sign.z / vec.z },
+ };
+
+ let mut percentage = Vec3 {
+ x: percentage_step.x
+ * if vec_sign.x > 0. {
+ 1. - right_before_start.x.fract()
+ } else {
+ right_before_start.x.fract().abs()
+ },
+ y: percentage_step.y
+ * if vec_sign.y > 0. {
+ 1. - right_before_start.y.fract()
+ } else {
+ right_before_start.y.fract().abs()
+ },
+ z: percentage_step.z
+ * if vec_sign.z > 0. {
+ 1. - right_before_start.z.fract()
+ } else {
+ right_before_start.z.fract().abs()
+ },
+ };
+
+ loop {
+ if percentage.x > 1. && percentage.y > 1. && percentage.z > 1. {
+ return get_miss_result(&context);
+ }
+
+ if percentage.x < percentage.y {
+ if percentage.x < percentage.z {
+ current_block.x += vec_sign.x as i32;
+ percentage.x += percentage_step.x;
+ } else {
+ current_block.z += vec_sign.z as i32;
+ percentage.z += percentage_step.z;
+ }
+ } else if percentage.y < percentage.z {
+ current_block.y += vec_sign.y as i32;
+ percentage.y += percentage_step.y;
+ } else {
+ current_block.z += vec_sign.z as i32;
+ percentage.z += percentage_step.z;
+ }
+
+ if let Some(data) = get_hit_result(&context, &current_block) {
+ return data;
+ }
+ }
+}
diff --git a/azalea-physics/src/collision/discrete_voxel_shape.rs b/azalea-physics/src/collision/discrete_voxel_shape.rs
index 4a329398..2bcd1f61 100755
--- a/azalea-physics/src/collision/discrete_voxel_shape.rs
+++ b/azalea-physics/src/collision/discrete_voxel_shape.rs
@@ -45,6 +45,7 @@ impl DiscreteVoxelShape {
return false;
}
let (x, y, z) = (x as u32, y as u32, z as u32);
+
(x < self.size(Axis::X) && y < self.size(Axis::Y) && z < self.size(Axis::Z))
&& (self.is_full(x, y, z))
}
diff --git a/azalea-physics/src/collision/mod.rs b/azalea-physics/src/collision/mod.rs
index 53efd2fe..a99b5710 100644
--- a/azalea-physics/src/collision/mod.rs
+++ b/azalea-physics/src/collision/mod.rs
@@ -5,10 +5,7 @@ mod shape;
mod world_collisions;
use azalea_core::{Axis, Vec3, AABB, EPSILON};
-use azalea_world::{
- entity::{self},
- Instance, MoveEntityError,
-};
+use azalea_world::{entity, Instance, MoveEntityError};
pub use blocks::BlockWithShape;
pub use discrete_voxel_shape::*;
pub use shape::*;
@@ -219,7 +216,11 @@ fn collide_with_shapes(
if y_movement != 0. {
y_movement = Shapes::collide(&Axis::Y, &entity_box, collision_boxes, y_movement);
if y_movement != 0. {
- entity_box = entity_box.move_relative(0., y_movement, 0.);
+ entity_box = entity_box.move_relative(&Vec3 {
+ x: 0.,
+ y: y_movement,
+ z: 0.,
+ });
}
}
@@ -230,14 +231,22 @@ fn collide_with_shapes(
if more_z_movement && z_movement != 0. {
z_movement = Shapes::collide(&Axis::Z, &entity_box, collision_boxes, z_movement);
if z_movement != 0. {
- entity_box = entity_box.move_relative(0., 0., z_movement);
+ entity_box = entity_box.move_relative(&Vec3 {
+ x: 0.,
+ y: 0.,
+ z: z_movement,
+ });
}
}
if x_movement != 0. {
x_movement = Shapes::collide(&Axis::X, &entity_box, collision_boxes, x_movement);
if x_movement != 0. {
- entity_box = entity_box.move_relative(x_movement, 0., 0.);
+ entity_box = entity_box.move_relative(&Vec3 {
+ x: x_movement,
+ y: 0.,
+ z: 0.,
+ });
}
}
diff --git a/azalea-physics/src/collision/shape.rs b/azalea-physics/src/collision/shape.rs
index cc184591..29c1b440 100755
--- a/azalea-physics/src/collision/shape.rs
+++ b/azalea-physics/src/collision/shape.rs
@@ -1,9 +1,11 @@
use super::mergers::IndexMerger;
use crate::collision::{BitSetDiscreteVoxelShape, DiscreteVoxelShape, AABB};
-use azalea_core::{binary_search, Axis, AxisCycle, EPSILON};
+use azalea_core::{
+ binary_search, Axis, AxisCycle, BlockHitResult, BlockPos, Direction, Vec3, EPSILON,
+};
use std::{cmp, num::NonZeroU32};
-pub struct Shapes {}
+pub struct Shapes;
pub fn block_shape() -> VoxelShape {
let mut shape = BitSetDiscreteVoxelShape::new(1, 1, 1);
@@ -390,6 +392,33 @@ impl VoxelShape {
}
}
+ pub fn clip(&self, from: &Vec3, to: &Vec3, block_pos: &BlockPos) -> Option<BlockHitResult> {
+ if self.is_empty() {
+ return None;
+ }
+ let vector = to - from;
+ if vector.length_sqr() < EPSILON {
+ return None;
+ }
+ let right_after_start = from + &(vector * 0.0001);
+
+ if self.shape().is_full_wide(
+ self.find_index(Axis::X, right_after_start.x - block_pos.x as f64),
+ self.find_index(Axis::Y, right_after_start.y - block_pos.y as f64),
+ self.find_index(Axis::Z, right_after_start.z - block_pos.z as f64),
+ ) {
+ Some(BlockHitResult {
+ block_pos: *block_pos,
+ direction: Direction::nearest(vector).opposite(),
+ location: right_after_start,
+ inside: true,
+ miss: false,
+ })
+ } else {
+ AABB::clip_iterable(&self.to_aabbs(), from, to, block_pos)
+ }
+ }
+
pub fn collide(&self, axis: &Axis, entity_box: &AABB, movement: f64) -> f64 {
self.collide_x(AxisCycle::between(*axis, Axis::X), entity_box, movement)
}
@@ -531,19 +560,34 @@ impl VoxelShape {
let y_coords = self.get_coords(Axis::Y);
let z_coords = self.get_coords(Axis::Z);
self.shape().for_all_boxes(
- |var4x, var5, var6, var7, var8, var9| {
+ |min_x, min_y, min_z, max_x, max_y, max_z| {
consumer(
- x_coords[var4x as usize],
- y_coords[var5 as usize],
- z_coords[var6 as usize],
- x_coords[var7 as usize],
- y_coords[var8 as usize],
- z_coords[var9 as usize],
+ x_coords[min_x as usize],
+ y_coords[min_y as usize],
+ z_coords[min_z as usize],
+ x_coords[max_x as usize],
+ y_coords[max_y as usize],
+ z_coords[max_z as usize],
);
},
true,
);
}
+
+ pub fn to_aabbs(&self) -> Vec<AABB> {
+ let mut aabbs = Vec::new();
+ self.for_all_boxes(|min_x, min_y, min_z, max_x, max_y, max_z| {
+ aabbs.push(AABB {
+ min_x,
+ min_y,
+ min_z,
+ max_x,
+ max_y,
+ max_z,
+ });
+ });
+ aabbs
+ }
}
impl From<AABB> for VoxelShape {
diff --git a/azalea-physics/src/lib.rs b/azalea-physics/src/lib.rs
index 049091f7..57c2100e 100644
--- a/azalea-physics/src/lib.rs
+++ b/azalea-physics/src/lib.rs
@@ -1,14 +1,15 @@
#![doc = include_str!("../README.md")]
#![feature(trait_alias)]
+pub mod clip;
pub mod collision;
use azalea_block::{Block, BlockState};
use azalea_core::{BlockPos, Vec3};
use azalea_world::{
entity::{
- metadata::Sprinting, move_relative, Attributes, Jumping, Local, Physics, Position,
- WorldName,
+ clamp_look_direction, metadata::Sprinting, move_relative, Attributes, Jumping, Local,
+ LookDirection, Physics, Position, WorldName,
},
Instance, InstanceContainer,
};
@@ -30,7 +31,11 @@ pub struct PhysicsPlugin;
impl Plugin for PhysicsPlugin {
fn build(&self, app: &mut App) {
app.add_event::<ForceJumpEvent>()
- .add_system(force_jump_listener.before(azalea_world::entity::update_bounding_box))
+ .add_system(
+ force_jump_listener
+ .before(azalea_world::entity::update_bounding_box)
+ .after(clamp_look_direction),
+ )
.add_systems(
(ai_step, travel)
.chain()
@@ -43,11 +48,20 @@ impl Plugin for PhysicsPlugin {
/// Move the entity with the given acceleration while handling friction,
/// gravity, collisions, and some other stuff.
fn travel(
- mut query: Query<(&mut Physics, &mut Position, &Attributes, &WorldName), With<Local>>,
- world_container: Res<InstanceContainer>,
+ mut query: Query<
+ (
+ &mut Physics,
+ &mut LookDirection,
+ &mut Position,
+ &Attributes,
+ &WorldName,
+ ),
+ With<Local>,
+ >,
+ instance_container: Res<InstanceContainer>,
) {
- for (mut physics, mut position, attributes, world_name) in &mut query {
- let world_lock = world_container
+ for (mut physics, direction, mut position, attributes, world_name) in &mut query {
+ let world_lock = instance_container
.get(world_name)
.expect("All entities should be in a valid world");
let world = world_lock.read();
@@ -85,6 +99,7 @@ fn travel(
block_friction,
&world,
&mut physics,
+ &direction,
&mut position,
attributes,
);
@@ -158,13 +173,21 @@ pub fn ai_step(
pub struct ForceJumpEvent(pub Entity);
pub fn force_jump_listener(
- mut query: Query<(&mut Physics, &Position, &Sprinting, &WorldName)>,
- world_container: Res<InstanceContainer>,
+ mut query: Query<(
+ &mut Physics,
+ &Position,
+ &LookDirection,
+ &Sprinting,
+ &WorldName,
+ )>,
+ instance_container: Res<InstanceContainer>,
mut events: EventReader<ForceJumpEvent>,
) {
for event in events.iter() {
- if let Ok((mut physics, position, sprinting, world_name)) = query.get_mut(event.0) {
- let world_lock = world_container
+ if let Ok((mut physics, position, direction, sprinting, world_name)) =
+ query.get_mut(event.0)
+ {
+ let world_lock = instance_container
.get(world_name)
.expect("All entities should be in a valid world");
let world = world_lock.read();
@@ -178,7 +201,7 @@ pub fn force_jump_listener(
};
if **sprinting {
// sprint jumping gives some extra velocity
- let y_rot = physics.y_rot * 0.017453292;
+ let y_rot = direction.y_rot * 0.017453292;
physics.delta += Vec3 {
x: (-f32::sin(y_rot) * 0.2) as f64,
y: 0.,
@@ -204,11 +227,13 @@ fn handle_relative_friction_and_calculate_movement(
block_friction: f32,
world: &Instance,
physics: &mut Physics,
+ direction: &LookDirection,
position: &mut Position,
attributes: &Attributes,
) -> Vec3 {
move_relative(
physics,
+ direction,
get_friction_influenced_speed(physics, attributes, block_friction),
&Vec3 {
x: physics.xxa as f64,
diff --git a/azalea-protocol/Cargo.toml b/azalea-protocol/Cargo.toml
index f61f076e..f555af4e 100644
--- a/azalea-protocol/Cargo.toml
+++ b/azalea-protocol/Cargo.toml
@@ -25,6 +25,7 @@ azalea-core = { path = "../azalea-core", optional = true, version = "^0.6.0", fe
"serde",
] }
azalea-crypto = { path = "../azalea-crypto", version = "^0.6.0" }
+azalea-inventory = { version = "0.1.0", path = "../azalea-inventory" }
azalea-nbt = { path = "../azalea-nbt", version = "^0.6.0", features = [
"serde",
] }
diff --git a/azalea-protocol/src/packets/game/clientbound_container_set_content_packet.rs b/azalea-protocol/src/packets/game/clientbound_container_set_content_packet.rs
index 0e9ce32b..4e08232d 100755
--- a/azalea-protocol/src/packets/game/clientbound_container_set_content_packet.rs
+++ b/azalea-protocol/src/packets/game/clientbound_container_set_content_packet.rs
@@ -1,12 +1,12 @@
use azalea_buf::McBuf;
-use azalea_core::Slot;
+use azalea_inventory::ItemSlot;
use azalea_protocol_macros::ClientboundGamePacket;
#[derive(Clone, Debug, McBuf, ClientboundGamePacket)]
pub struct ClientboundContainerSetContentPacket {
- pub container_id: u8,
+ pub container_id: i8,
#[var]
pub state_id: u32,
- pub items: Vec<Slot>,
- pub carried_item: Slot,
+ pub items: Vec<ItemSlot>,
+ pub carried_item: ItemSlot,
}
diff --git a/azalea-protocol/src/packets/game/clientbound_container_set_data_packet.rs b/azalea-protocol/src/packets/game/clientbound_container_set_data_packet.rs
index e09c16d7..dc53a024 100755
--- a/azalea-protocol/src/packets/game/clientbound_container_set_data_packet.rs
+++ b/azalea-protocol/src/packets/game/clientbound_container_set_data_packet.rs
@@ -3,7 +3,7 @@ use azalea_protocol_macros::ClientboundGamePacket;
#[derive(Clone, Debug, McBuf, ClientboundGamePacket)]
pub struct ClientboundContainerSetDataPacket {
- pub container_id: u8,
+ pub container_id: i8,
pub id: u16,
pub value: u16,
}
diff --git a/azalea-protocol/src/packets/game/clientbound_container_set_slot_packet.rs b/azalea-protocol/src/packets/game/clientbound_container_set_slot_packet.rs
index 0ed249a9..9b954fa0 100755
--- a/azalea-protocol/src/packets/game/clientbound_container_set_slot_packet.rs
+++ b/azalea-protocol/src/packets/game/clientbound_container_set_slot_packet.rs
@@ -1,12 +1,12 @@
use azalea_buf::McBuf;
-use azalea_core::Slot;
+use azalea_inventory::ItemSlot;
use azalea_protocol_macros::ClientboundGamePacket;
#[derive(Clone, Debug, McBuf, ClientboundGamePacket)]
pub struct ClientboundContainerSetSlotPacket {
- pub container_id: u8,
+ pub container_id: i8,
#[var]
pub state_id: u32,
pub slot: u16,
- pub item_stack: Slot,
+ pub item_stack: ItemSlot,
}
diff --git a/azalea-protocol/src/packets/game/clientbound_login_packet.rs b/azalea-protocol/src/packets/game/clientbound_login_packet.rs
index a35951a7..bafead86 100755
--- a/azalea-protocol/src/packets/game/clientbound_login_packet.rs
+++ b/azalea-protocol/src/packets/game/clientbound_login_packet.rs
@@ -1,6 +1,6 @@
use self::registry::RegistryHolder;
use azalea_buf::McBuf;
-use azalea_core::{GameType, GlobalPos, OptionalGameType, ResourceLocation};
+use azalea_core::{GameMode, GlobalPos, OptionalGameType, ResourceLocation};
use azalea_protocol_macros::ClientboundGamePacket;
/// The first packet sent by the server to the client after login.
@@ -11,7 +11,7 @@ use azalea_protocol_macros::ClientboundGamePacket;
pub struct ClientboundLoginPacket {
pub player_id: u32,
pub hardcore: bool,
- pub game_type: GameType,
+ pub game_type: GameMode,
pub previous_game_type: OptionalGameType,
pub levels: Vec<ResourceLocation>,
pub registry_holder: RegistryHolder,
diff --git a/azalea-protocol/src/packets/game/clientbound_merchant_offers_packet.rs b/azalea-protocol/src/packets/game/clientbound_merchant_offers_packet.rs
index 21ac8b2e..4253ace4 100755
--- a/azalea-protocol/src/packets/game/clientbound_merchant_offers_packet.rs
+++ b/azalea-protocol/src/packets/game/clientbound_merchant_offers_packet.rs
@@ -1,5 +1,5 @@
use azalea_buf::McBuf;
-use azalea_core::Slot;
+use azalea_inventory::ItemSlot;
use azalea_protocol_macros::ClientboundGamePacket;
#[derive(Clone, Debug, McBuf, ClientboundGamePacket)]
@@ -17,9 +17,9 @@ pub struct ClientboundMerchantOffersPacket {
#[derive(Clone, Debug, McBuf)]
pub struct MerchantOffer {
- pub base_cost_a: Slot,
- pub result: Slot,
- pub cost_b: Slot,
+ pub base_cost_a: ItemSlot,
+ pub result: ItemSlot,
+ pub cost_b: ItemSlot,
pub out_of_stock: bool,
pub uses: u32,
pub max_uses: u32,
diff --git a/azalea-protocol/src/packets/game/clientbound_open_screen_packet.rs b/azalea-protocol/src/packets/game/clientbound_open_screen_packet.rs
index 9b8b02a1..582cac17 100755
--- a/azalea-protocol/src/packets/game/clientbound_open_screen_packet.rs
+++ b/azalea-protocol/src/packets/game/clientbound_open_screen_packet.rs
@@ -6,6 +6,6 @@ use azalea_protocol_macros::ClientboundGamePacket;
pub struct ClientboundOpenScreenPacket {
#[var]
pub container_id: u32,
- pub menu_type: azalea_registry::Menu,
+ pub menu_type: azalea_registry::MenuKind,
pub title: FormattedText,
}
diff --git a/azalea-protocol/src/packets/game/clientbound_player_info_update_packet.rs b/azalea-protocol/src/packets/game/clientbound_player_info_update_packet.rs
index dc518c9c..1dad147f 100644
--- a/azalea-protocol/src/packets/game/clientbound_player_info_update_packet.rs
+++ b/azalea-protocol/src/packets/game/clientbound_player_info_update_packet.rs
@@ -3,7 +3,7 @@ use azalea_buf::{
BufReadError, McBuf, McBufReadable, McBufVarReadable, McBufVarWritable, McBufWritable,
};
use azalea_chat::FormattedText;
-use azalea_core::{FixedBitSet, GameType};
+use azalea_core::{FixedBitSet, GameMode};
use azalea_protocol_macros::ClientboundGamePacket;
use std::{
collections::HashMap,
@@ -24,7 +24,7 @@ pub struct PlayerInfoEntry {
pub profile: GameProfile,
pub listed: bool,
pub latency: i32,
- pub game_mode: GameType,
+ pub game_mode: GameMode,
pub display_name: Option<FormattedText>,
pub chat_session: Option<RemoteChatSessionData>,
}
@@ -40,7 +40,7 @@ pub struct InitializeChatAction {
}
#[derive(Clone, Debug, McBuf)]
pub struct UpdateGameModeAction {
- pub game_mode: GameType,
+ pub game_mode: GameMode,
}
#[derive(Clone, Debug, McBuf)]
pub struct UpdateListedAction {
diff --git a/azalea-protocol/src/packets/game/clientbound_respawn_packet.rs b/azalea-protocol/src/packets/game/clientbound_respawn_packet.rs
index 58488124..03cc2917 100755
--- a/azalea-protocol/src/packets/game/clientbound_respawn_packet.rs
+++ b/azalea-protocol/src/packets/game/clientbound_respawn_packet.rs
@@ -1,5 +1,5 @@
use azalea_buf::McBuf;
-use azalea_core::{GameType, GlobalPos, OptionalGameType, ResourceLocation};
+use azalea_core::{GameMode, GlobalPos, OptionalGameType, ResourceLocation};
use azalea_protocol_macros::ClientboundGamePacket;
#[derive(Clone, Debug, McBuf, ClientboundGamePacket)]
@@ -7,7 +7,7 @@ pub struct ClientboundRespawnPacket {
pub dimension_type: ResourceLocation,
pub dimension: ResourceLocation,
pub seed: u64,
- pub player_game_type: GameType,
+ pub player_game_type: GameMode,
pub previous_player_game_type: OptionalGameType,
pub is_debug: bool,
pub is_flat: bool,
diff --git a/azalea-protocol/src/packets/game/clientbound_set_equipment_packet.rs b/azalea-protocol/src/packets/game/clientbound_set_equipment_packet.rs
index 11472591..0acdc687 100755
--- a/azalea-protocol/src/packets/game/clientbound_set_equipment_packet.rs
+++ b/azalea-protocol/src/packets/game/clientbound_set_equipment_packet.rs
@@ -1,6 +1,6 @@
use azalea_buf::{BufReadError, McBuf};
use azalea_buf::{McBufReadable, McBufWritable};
-use azalea_core::Slot;
+use azalea_inventory::ItemSlot;
use azalea_protocol_macros::ClientboundGamePacket;
use std::io::Cursor;
@@ -13,7 +13,7 @@ pub struct ClientboundSetEquipmentPacket {
#[derive(Clone, Debug)]
pub struct EquipmentSlots {
- pub slots: Vec<(EquipmentSlot, Slot)>,
+ pub slots: Vec<(EquipmentSlot, ItemSlot)>,
}
impl McBufReadable for EquipmentSlots {
@@ -28,7 +28,7 @@ impl McBufReadable for EquipmentSlots {
id: equipment_byte.into(),
}
})?;
- let item = Slot::read_from(buf)?;
+ let item = ItemSlot::read_from(buf)?;
slots.push((equipment_slot, item));
if equipment_byte & 128 == 0 {
break;
diff --git a/azalea-protocol/src/packets/game/clientbound_update_advancements_packet.rs b/azalea-protocol/src/packets/game/clientbound_update_advancements_packet.rs
index dcc6332a..76f1412c 100755
--- a/azalea-protocol/src/packets/game/clientbound_update_advancements_packet.rs
+++ b/azalea-protocol/src/packets/game/clientbound_update_advancements_packet.rs
@@ -1,6 +1,7 @@
use azalea_buf::McBuf;
use azalea_chat::FormattedText;
-use azalea_core::{ResourceLocation, Slot};
+use azalea_core::ResourceLocation;
+use azalea_inventory::ItemSlot;
use azalea_protocol_macros::ClientboundGamePacket;
use std::collections::HashMap;
use std::io::Cursor;
@@ -25,7 +26,7 @@ pub struct Advancement {
pub struct DisplayInfo {
pub title: FormattedText,
pub description: FormattedText,
- pub icon: Slot,
+ pub icon: ItemSlot,
pub frame: FrameType,
pub show_toast: bool,
pub hidden: bool,
@@ -130,7 +131,7 @@ mod tests {
display: Some(DisplayInfo {
title: FormattedText::from("title".to_string()),
description: FormattedText::from("description".to_string()),
- icon: Slot::Empty,
+ icon: ItemSlot::Empty,
frame: FrameType::Task,
show_toast: true,
hidden: false,
diff --git a/azalea-protocol/src/packets/game/clientbound_update_recipes_packet.rs b/azalea-protocol/src/packets/game/clientbound_update_recipes_packet.rs
index 318adb7f..94fe31c1 100755
--- a/azalea-protocol/src/packets/game/clientbound_update_recipes_packet.rs
+++ b/azalea-protocol/src/packets/game/clientbound_update_recipes_packet.rs
@@ -1,7 +1,8 @@
use azalea_buf::{
BufReadError, McBuf, McBufReadable, McBufVarReadable, McBufVarWritable, McBufWritable,
};
-use azalea_core::{ResourceLocation, Slot};
+use azalea_core::ResourceLocation;
+use azalea_inventory::ItemSlot;
use azalea_protocol_macros::ClientboundGamePacket;
use azalea_registry::RecipeSerializer;
@@ -26,7 +27,7 @@ pub struct ShapelessRecipe {
pub group: String,
pub category: CraftingBookCategory,
pub ingredients: Vec<Ingredient>,
- pub result: Slot,
+ pub result: ItemSlot,
}
#[derive(Clone, Debug)]
pub struct ShapedRecipe {
@@ -35,7 +36,7 @@ pub struct ShapedRecipe {
pub group: String,
pub category: CraftingBookCategory,
pub ingredients: Vec<Ingredient>,
- pub result: Slot,
+ pub result: ItemSlot,
pub show_notification: bool,
}
@@ -71,7 +72,7 @@ impl McBufReadable for ShapedRecipe {
for _ in 0..width * height {
ingredients.push(Ingredient::read_from(buf)?);
}
- let result = Slot::read_from(buf)?;
+ let result = ItemSlot::read_from(buf)?;
let show_notification = bool::read_from(buf)?;
Ok(ShapedRecipe {
@@ -91,7 +92,7 @@ pub struct CookingRecipe {
pub group: String,
pub category: CraftingBookCategory,
pub ingredient: Ingredient,
- pub result: Slot,
+ pub result: ItemSlot,
pub experience: f32,
#[var]
pub cooking_time: u32,
@@ -100,13 +101,13 @@ pub struct CookingRecipe {
pub struct StoneCutterRecipe {
pub group: String,
pub ingredient: Ingredient,
- pub result: Slot,
+ pub result: ItemSlot,
}
#[derive(Clone, Debug, McBuf)]
pub struct SmithingRecipe {
pub base: Ingredient,
pub addition: Ingredient,
- pub result: Slot,
+ pub result: ItemSlot,
}
#[derive(Clone, Debug, McBuf)]
@@ -119,7 +120,7 @@ pub struct SmithingTransformRecipe {
pub template: Ingredient,
pub base: Ingredient,
pub addition: Ingredient,
- pub result: Slot,
+ pub result: ItemSlot,
}
#[derive(Clone, Debug, McBuf)]
@@ -159,7 +160,7 @@ pub enum RecipeData {
#[derive(Clone, Debug, McBuf)]
pub struct Ingredient {
- pub allowed: Vec<Slot>,
+ pub allowed: Vec<ItemSlot>,
}
impl McBufWritable for Recipe {
diff --git a/azalea-protocol/src/packets/game/serverbound_container_click_packet.rs b/azalea-protocol/src/packets/game/serverbound_container_click_packet.rs
index 768d3f94..119af220 100755
--- a/azalea-protocol/src/packets/game/serverbound_container_click_packet.rs
+++ b/azalea-protocol/src/packets/game/serverbound_container_click_packet.rs
@@ -1,5 +1,5 @@
use azalea_buf::McBuf;
-use azalea_core::Slot;
+use azalea_inventory::{operations::ClickType, ItemSlot};
use azalea_protocol_macros::ServerboundGamePacket;
use std::collections::HashMap;
@@ -8,20 +8,9 @@ pub struct ServerboundContainerClickPacket {
pub container_id: u8,
#[var]
pub state_id: u32,
- pub slot_num: u16,
+ pub slot_num: i16,
pub button_num: u8,
pub click_type: ClickType,
- pub changed_slots: HashMap<u16, Slot>,
- pub carried_item: Slot,
-}
-
-#[derive(McBuf, Clone, Copy, Debug)]
-pub enum ClickType {
- Pickup = 0,
- QuickMove = 1,
- Swap = 2,
- Clone = 3,
- Throw = 4,
- QuickCraft = 5,
- PickupAll = 6,
+ pub changed_slots: HashMap<u16, ItemSlot>,
+ pub carried_item: ItemSlot,
}
diff --git a/azalea-protocol/src/packets/game/serverbound_set_creative_mode_slot_packet.rs b/azalea-protocol/src/packets/game/serverbound_set_creative_mode_slot_packet.rs
index 254950de..7730bf5a 100755
--- a/azalea-protocol/src/packets/game/serverbound_set_creative_mode_slot_packet.rs
+++ b/azalea-protocol/src/packets/game/serverbound_set_creative_mode_slot_packet.rs
@@ -1,9 +1,9 @@
use azalea_buf::McBuf;
-use azalea_core::Slot;
+use azalea_inventory::ItemSlot;
use azalea_protocol_macros::ServerboundGamePacket;
#[derive(Clone, Debug, McBuf, ServerboundGamePacket)]
pub struct ServerboundSetCreativeModeSlotPacket {
pub slot_num: u16,
- pub item_stack: Slot,
+ pub item_stack: ItemSlot,
}
diff --git a/azalea-protocol/src/packets/game/serverbound_use_item_on_packet.rs b/azalea-protocol/src/packets/game/serverbound_use_item_on_packet.rs
index c7a32a8f..50cbe914 100755
--- a/azalea-protocol/src/packets/game/serverbound_use_item_on_packet.rs
+++ b/azalea-protocol/src/packets/game/serverbound_use_item_on_packet.rs
@@ -7,20 +7,26 @@ use std::io::{Cursor, Write};
#[derive(Clone, Debug, McBuf, ServerboundGamePacket)]
pub struct ServerboundUseItemOnPacket {
pub hand: InteractionHand,
- pub block_hit: BlockHitResult,
+ pub block_hit: BlockHit,
#[var]
pub sequence: u32,
}
#[derive(Clone, Debug)]
-pub struct BlockHitResult {
+pub struct BlockHit {
+ /// The block that we clicked.
pub block_pos: BlockPos,
+ /// The face of the block that was clicked.
pub direction: Direction,
+ /// The exact coordinates of the world where the block was clicked. In the
+ /// network, this is transmitted as the difference between the location and
+ /// block position.
pub location: Vec3,
+ /// Whether the player's head is inside of a block.
pub inside: bool,
}
-impl McBufWritable for BlockHitResult {
+impl McBufWritable for BlockHit {
fn write_into(&self, buf: &mut impl Write) -> Result<(), std::io::Error> {
self.block_pos.write_into(buf)?;
self.direction.write_into(buf)?;
@@ -41,7 +47,7 @@ impl McBufWritable for BlockHitResult {
}
}
-impl McBufReadable for BlockHitResult {
+impl McBufReadable for BlockHit {
fn read_from(buf: &mut Cursor<&[u8]>) -> Result<Self, BufReadError> {
let block_pos = BlockPos::read_from(buf)?;
let direction = Direction::read_from(buf)?;
diff --git a/azalea-registry/src/lib.rs b/azalea-registry/src/lib.rs
index 19af0eea..c83c97f5 100755
--- a/azalea-registry/src/lib.rs
+++ b/azalea-registry/src/lib.rs
@@ -3093,7 +3093,7 @@ enum MemoryModuleKind {
}
registry! {
-enum Menu {
+enum MenuKind {
Generic9x1 => "minecraft:generic_9x1",
Generic9x2 => "minecraft:generic_9x2",
Generic9x3 => "minecraft:generic_9x3",
@@ -5340,3 +5340,1236 @@ enum DecoratedPotPatterns {
DecoratedPotBase => "minecraft:decorated_pot_base",
}
}
+
+registry! {
+enum ItemKind {
+ Air => "minecraft:air",
+ Stone => "minecraft:stone",
+ Granite => "minecraft:granite",
+ PolishedGranite => "minecraft:polished_granite",
+ Diorite => "minecraft:diorite",
+ PolishedDiorite => "minecraft:polished_diorite",
+ Andesite => "minecraft:andesite",
+ PolishedAndesite => "minecraft:polished_andesite",
+ Deepslate => "minecraft:deepslate",
+ CobbledDeepslate => "minecraft:cobbled_deepslate",
+ PolishedDeepslate => "minecraft:polished_deepslate",
+ Calcite => "minecraft:calcite",
+ Tuff => "minecraft:tuff",
+ DripstoneBlock => "minecraft:dripstone_block",
+ GrassBlock => "minecraft:grass_block",
+ Dirt => "minecraft:dirt",
+ CoarseDirt => "minecraft:coarse_dirt",
+ Podzol => "minecraft:podzol",
+ RootedDirt => "minecraft:rooted_dirt",
+ Mud => "minecraft:mud",
+ CrimsonNylium => "minecraft:crimson_nylium",
+ WarpedNylium => "minecraft:warped_nylium",
+ Cobblestone => "minecraft:cobblestone",
+ OakPlanks => "minecraft:oak_planks",
+ SprucePlanks => "minecraft:spruce_planks",
+ BirchPlanks => "minecraft:birch_planks",
+ JunglePlanks => "minecraft:jungle_planks",
+ AcaciaPlanks => "minecraft:acacia_planks",
+ CherryPlanks => "minecraft:cherry_planks",
+ DarkOakPlanks => "minecraft:dark_oak_planks",
+ MangrovePlanks => "minecraft:mangrove_planks",
+ BambooPlanks => "minecraft:bamboo_planks",
+ CrimsonPlanks => "minecraft:crimson_planks",
+ WarpedPlanks => "minecraft:warped_planks",
+ BambooMosaic => "minecraft:bamboo_mosaic",
+ OakSapling => "minecraft:oak_sapling",
+ SpruceSapling => "minecraft:spruce_sapling",
+ BirchSapling => "minecraft:birch_sapling",
+ JungleSapling => "minecraft:jungle_sapling",
+ AcaciaSapling => "minecraft:acacia_sapling",
+ CherrySapling => "minecraft:cherry_sapling",
+ DarkOakSapling => "minecraft:dark_oak_sapling",
+ MangrovePropagule => "minecraft:mangrove_propagule",
+ Bedrock => "minecraft:bedrock",
+ Sand => "minecraft:sand",
+ SuspiciousSand => "minecraft:suspicious_sand",
+ RedSand => "minecraft:red_sand",
+ Gravel => "minecraft:gravel",
+ CoalOre => "minecraft:coal_ore",
+ DeepslateCoalOre => "minecraft:deepslate_coal_ore",
+ IronOre => "minecraft:iron_ore",
+ DeepslateIronOre => "minecraft:deepslate_iron_ore",
+ CopperOre => "minecraft:copper_ore",
+ DeepslateCopperOre => "minecraft:deepslate_copper_ore",
+ GoldOre => "minecraft:gold_ore",
+ DeepslateGoldOre => "minecraft:deepslate_gold_ore",
+ RedstoneOre => "minecraft:redstone_ore",
+ DeepslateRedstoneOre => "minecraft:deepslate_redstone_ore",
+ EmeraldOre => "minecraft:emerald_ore",
+ DeepslateEmeraldOre => "minecraft:deepslate_emerald_ore",
+ LapisOre => "minecraft:lapis_ore",
+ DeepslateLapisOre => "minecraft:deepslate_lapis_ore",
+ DiamondOre => "minecraft:diamond_ore",
+ DeepslateDiamondOre => "minecraft:deepslate_diamond_ore",
+ NetherGoldOre => "minecraft:nether_gold_ore",
+ NetherQuartzOre => "minecraft:nether_quartz_ore",
+ AncientDebris => "minecraft:ancient_debris",
+ CoalBlock => "minecraft:coal_block",
+ RawIronBlock => "minecraft:raw_iron_block",
+ RawCopperBlock => "minecraft:raw_copper_block",
+ RawGoldBlock => "minecraft:raw_gold_block",
+ AmethystBlock => "minecraft:amethyst_block",
+ BuddingAmethyst => "minecraft:budding_amethyst",
+ IronBlock => "minecraft:iron_block",
+ CopperBlock => "minecraft:copper_block",
+ GoldBlock => "minecraft:gold_block",
+ DiamondBlock => "minecraft:diamond_block",
+ NetheriteBlock => "minecraft:netherite_block",
+ ExposedCopper => "minecraft:exposed_copper",
+ WeatheredCopper => "minecraft:weathered_copper",
+ OxidizedCopper => "minecraft:oxidized_copper",
+ CutCopper => "minecraft:cut_copper",
+ ExposedCutCopper => "minecraft:exposed_cut_copper",
+ WeatheredCutCopper => "minecraft:weathered_cut_copper",
+ OxidizedCutCopper => "minecraft:oxidized_cut_copper",
+ CutCopperStairs => "minecraft:cut_copper_stairs",
+ ExposedCutCopperStairs => "minecraft:exposed_cut_copper_stairs",
+ WeatheredCutCopperStairs => "minecraft:weathered_cut_copper_stairs",
+ OxidizedCutCopperStairs => "minecraft:oxidized_cut_copper_stairs",
+ CutCopperSlab => "minecraft:cut_copper_slab",
+ ExposedCutCopperSlab => "minecraft:exposed_cut_copper_slab",
+ WeatheredCutCopperSlab => "minecraft:weathered_cut_copper_slab",
+ OxidizedCutCopperSlab => "minecraft:oxidized_cut_copper_slab",
+ WaxedCopperBlock => "minecraft:waxed_copper_block",
+ WaxedExposedCopper => "minecraft:waxed_exposed_copper",
+ WaxedWeatheredCopper => "minecraft:waxed_weathered_copper",
+ WaxedOxidizedCopper => "minecraft:waxed_oxidized_copper",
+ WaxedCutCopper => "minecraft:waxed_cut_copper",
+ WaxedExposedCutCopper => "minecraft:waxed_exposed_cut_copper",
+ WaxedWeatheredCutCopper => "minecraft:waxed_weathered_cut_copper",
+ WaxedOxidizedCutCopper => "minecraft:waxed_oxidized_cut_copper",
+ WaxedCutCopperStairs => "minecraft:waxed_cut_copper_stairs",
+ WaxedExposedCutCopperStairs => "minecraft:waxed_exposed_cut_copper_stairs",
+ WaxedWeatheredCutCopperStairs => "minecraft:waxed_weathered_cut_copper_stairs",
+ WaxedOxidizedCutCopperStairs => "minecraft:waxed_oxidized_cut_copper_stairs",
+ WaxedCutCopperSlab => "minecraft:waxed_cut_copper_slab",
+ WaxedExposedCutCopperSlab => "minecraft:waxed_exposed_cut_copper_slab",
+ WaxedWeatheredCutCopperSlab => "minecraft:waxed_weathered_cut_copper_slab",
+ WaxedOxidizedCutCopperSlab => "minecraft:waxed_oxidized_cut_copper_slab",
+ OakLog => "minecraft:oak_log",
+ SpruceLog => "minecraft:spruce_log",
+ BirchLog => "minecraft:birch_log",
+ JungleLog => "minecraft:jungle_log",
+ AcaciaLog => "minecraft:acacia_log",
+ CherryLog => "minecraft:cherry_log",
+ DarkOakLog => "minecraft:dark_oak_log",
+ MangroveLog => "minecraft:mangrove_log",
+ MangroveRoots => "minecraft:mangrove_roots",
+ MuddyMangroveRoots => "minecraft:muddy_mangrove_roots",
+ CrimsonStem => "minecraft:crimson_stem",
+ WarpedStem => "minecraft:warped_stem",
+ BambooBlock => "minecraft:bamboo_block",
+ StrippedOakLog => "minecraft:stripped_oak_log",
+ StrippedSpruceLog => "minecraft:stripped_spruce_log",
+ StrippedBirchLog => "minecraft:stripped_birch_log",
+ StrippedJungleLog => "minecraft:stripped_jungle_log",
+ StrippedAcaciaLog => "minecraft:stripped_acacia_log",
+ StrippedCherryLog => "minecraft:stripped_cherry_log",
+ StrippedDarkOakLog => "minecraft:stripped_dark_oak_log",
+ StrippedMangroveLog => "minecraft:stripped_mangrove_log",
+ StrippedCrimsonStem => "minecraft:stripped_crimson_stem",
+ StrippedWarpedStem => "minecraft:stripped_warped_stem",
+ StrippedOakWood => "minecraft:stripped_oak_wood",
+ StrippedSpruceWood => "minecraft:stripped_spruce_wood",
+ StrippedBirchWood => "minecraft:stripped_birch_wood",
+ StrippedJungleWood => "minecraft:stripped_jungle_wood",
+ StrippedAcaciaWood => "minecraft:stripped_acacia_wood",
+ StrippedCherryWood => "minecraft:stripped_cherry_wood",
+ StrippedDarkOakWood => "minecraft:stripped_dark_oak_wood",
+ StrippedMangroveWood => "minecraft:stripped_mangrove_wood",
+ StrippedCrimsonHyphae => "minecraft:stripped_crimson_hyphae",
+ StrippedWarpedHyphae => "minecraft:stripped_warped_hyphae",
+ StrippedBambooBlock => "minecraft:stripped_bamboo_block",
+ OakWood => "minecraft:oak_wood",
+ SpruceWood => "minecraft:spruce_wood",
+ BirchWood => "minecraft:birch_wood",
+ JungleWood => "minecraft:jungle_wood",
+ AcaciaWood => "minecraft:acacia_wood",
+ CherryWood => "minecraft:cherry_wood",
+ DarkOakWood => "minecraft:dark_oak_wood",
+ MangroveWood => "minecraft:mangrove_wood",
+ CrimsonHyphae => "minecraft:crimson_hyphae",
+ WarpedHyphae => "minecraft:warped_hyphae",
+ OakLeaves => "minecraft:oak_leaves",
+ SpruceLeaves => "minecraft:spruce_leaves",
+ BirchLeaves => "minecraft:birch_leaves",
+ JungleLeaves => "minecraft:jungle_leaves",
+ AcaciaLeaves => "minecraft:acacia_leaves",
+ CherryLeaves => "minecraft:cherry_leaves",
+ DarkOakLeaves => "minecraft:dark_oak_leaves",
+ MangroveLeaves => "minecraft:mangrove_leaves",
+ AzaleaLeaves => "minecraft:azalea_leaves",
+ FloweringAzaleaLeaves => "minecraft:flowering_azalea_leaves",
+ Sponge => "minecraft:sponge",
+ WetSponge => "minecraft:wet_sponge",
+ Glass => "minecraft:glass",
+ TintedGlass => "minecraft:tinted_glass",
+ LapisBlock => "minecraft:lapis_block",
+ Sandstone => "minecraft:sandstone",
+ ChiseledSandstone => "minecraft:chiseled_sandstone",
+ CutSandstone => "minecraft:cut_sandstone",
+ Cobweb => "minecraft:cobweb",
+ Grass => "minecraft:grass",
+ Fern => "minecraft:fern",
+ Azalea => "minecraft:azalea",
+ FloweringAzalea => "minecraft:flowering_azalea",
+ DeadBush => "minecraft:dead_bush",
+ Seagrass => "minecraft:seagrass",
+ SeaPickle => "minecraft:sea_pickle",
+ WhiteWool => "minecraft:white_wool",
+ OrangeWool => "minecraft:orange_wool",
+ MagentaWool => "minecraft:magenta_wool",
+ LightBlueWool => "minecraft:light_blue_wool",
+ YellowWool => "minecraft:yellow_wool",
+ LimeWool => "minecraft:lime_wool",
+ PinkWool => "minecraft:pink_wool",
+ GrayWool => "minecraft:gray_wool",
+ LightGrayWool => "minecraft:light_gray_wool",
+ CyanWool => "minecraft:cyan_wool",
+ PurpleWool => "minecraft:purple_wool",
+ BlueWool => "minecraft:blue_wool",
+ BrownWool => "minecraft:brown_wool",
+ GreenWool => "minecraft:green_wool",
+ RedWool => "minecraft:red_wool",
+ BlackWool => "minecraft:black_wool",
+ Dandelion => "minecraft:dandelion",
+ Poppy => "minecraft:poppy",
+ BlueOrchid => "minecraft:blue_orchid",
+ Allium => "minecraft:allium",
+ AzureBluet => "minecraft:azure_bluet",
+ RedTulip => "minecraft:red_tulip",
+ OrangeTulip => "minecraft:orange_tulip",
+ WhiteTulip => "minecraft:white_tulip",
+ PinkTulip => "minecraft:pink_tulip",
+ OxeyeDaisy => "minecraft:oxeye_daisy",
+ Cornflower => "minecraft:cornflower",
+ LilyOfTheValley => "minecraft:lily_of_the_valley",
+ WitherRose => "minecraft:wither_rose",
+ Torchflower => "minecraft:torchflower",
+ SporeBlossom => "minecraft:spore_blossom",
+ BrownMushroom => "minecraft:brown_mushroom",
+ RedMushroom => "minecraft:red_mushroom",
+ CrimsonFungus => "minecraft:crimson_fungus",
+ WarpedFungus => "minecraft:warped_fungus",
+ CrimsonRoots => "minecraft:crimson_roots",
+ WarpedRoots => "minecraft:warped_roots",
+ NetherSprouts => "minecraft:nether_sprouts",
+ WeepingVines => "minecraft:weeping_vines",
+ TwistingVines => "minecraft:twisting_vines",
+ SugarCane => "minecraft:sugar_cane",
+ Kelp => "minecraft:kelp",
+ MossCarpet => "minecraft:moss_carpet",
+ PinkPetals => "minecraft:pink_petals",
+ MossBlock => "minecraft:moss_block",
+ HangingRoots => "minecraft:hanging_roots",
+ BigDripleaf => "minecraft:big_dripleaf",
+ SmallDripleaf => "minecraft:small_dripleaf",
+ Bamboo => "minecraft:bamboo",
+ OakSlab => "minecraft:oak_slab",
+ SpruceSlab => "minecraft:spruce_slab",
+ BirchSlab => "minecraft:birch_slab",
+ JungleSlab => "minecraft:jungle_slab",
+ AcaciaSlab => "minecraft:acacia_slab",
+ CherrySlab => "minecraft:cherry_slab",
+ DarkOakSlab => "minecraft:dark_oak_slab",
+ MangroveSlab => "minecraft:mangrove_slab",
+ BambooSlab => "minecraft:bamboo_slab",
+ BambooMosaicSlab => "minecraft:bamboo_mosaic_slab",
+ CrimsonSlab => "minecraft:crimson_slab",
+ WarpedSlab => "minecraft:warped_slab",
+ StoneSlab => "minecraft:stone_slab",
+ SmoothStoneSlab => "minecraft:smooth_stone_slab",
+ SandstoneSlab => "minecraft:sandstone_slab",
+ CutSandstoneSlab => "minecraft:cut_sandstone_slab",
+ PetrifiedOakSlab => "minecraft:petrified_oak_slab",
+ CobblestoneSlab => "minecraft:cobblestone_slab",
+ BrickSlab => "minecraft:brick_slab",
+ StoneBrickSlab => "minecraft:stone_brick_slab",
+ MudBrickSlab => "minecraft:mud_brick_slab",
+ NetherBrickSlab => "minecraft:nether_brick_slab",
+ QuartzSlab => "minecraft:quartz_slab",
+ RedSandstoneSlab => "minecraft:red_sandstone_slab",
+ CutRedSandstoneSlab => "minecraft:cut_red_sandstone_slab",
+ PurpurSlab => "minecraft:purpur_slab",
+ PrismarineSlab => "minecraft:prismarine_slab",
+ PrismarineBrickSlab => "minecraft:prismarine_brick_slab",
+ DarkPrismarineSlab => "minecraft:dark_prismarine_slab",
+ SmoothQuartz => "minecraft:smooth_quartz",
+ SmoothRedSandstone => "minecraft:smooth_red_sandstone",
+ SmoothSandstone => "minecraft:smooth_sandstone",
+ SmoothStone => "minecraft:smooth_stone",
+ Bricks => "minecraft:bricks",
+ Bookshelf => "minecraft:bookshelf",
+ ChiseledBookshelf => "minecraft:chiseled_bookshelf",
+ DecoratedPot => "minecraft:decorated_pot",
+ MossyCobblestone => "minecraft:mossy_cobblestone",
+ Obsidian => "minecraft:obsidian",
+ Torch => "minecraft:torch",
+ EndRod => "minecraft:end_rod",
+ ChorusPlant => "minecraft:chorus_plant",
+ ChorusFlower => "minecraft:chorus_flower",
+ PurpurBlock => "minecraft:purpur_block",
+ PurpurPillar => "minecraft:purpur_pillar",
+ PurpurStairs => "minecraft:purpur_stairs",
+ Spawner => "minecraft:spawner",
+ Chest => "minecraft:chest",
+ CraftingTable => "minecraft:crafting_table",
+ Farmland => "minecraft:farmland",
+ Furnace => "minecraft:furnace",
+ Ladder => "minecraft:ladder",
+ CobblestoneStairs => "minecraft:cobblestone_stairs",
+ Snow => "minecraft:snow",
+ Ice => "minecraft:ice",
+ SnowBlock => "minecraft:snow_block",
+ Cactus => "minecraft:cactus",
+ Clay => "minecraft:clay",
+ Jukebox => "minecraft:jukebox",
+ OakFence => "minecraft:oak_fence",
+ SpruceFence => "minecraft:spruce_fence",
+ BirchFence => "minecraft:birch_fence",
+ JungleFence => "minecraft:jungle_fence",
+ AcaciaFence => "minecraft:acacia_fence",
+ CherryFence => "minecraft:cherry_fence",
+ DarkOakFence => "minecraft:dark_oak_fence",
+ MangroveFence => "minecraft:mangrove_fence",
+ BambooFence => "minecraft:bamboo_fence",
+ CrimsonFence => "minecraft:crimson_fence",
+ WarpedFence => "minecraft:warped_fence",
+ Pumpkin => "minecraft:pumpkin",
+ CarvedPumpkin => "minecraft:carved_pumpkin",
+ JackOLantern => "minecraft:jack_o_lantern",
+ Netherrack => "minecraft:netherrack",
+ SoulSand => "minecraft:soul_sand",
+ SoulSoil => "minecraft:soul_soil",
+ Basalt => "minecraft:basalt",
+ PolishedBasalt => "minecraft:polished_basalt",
+ SmoothBasalt => "minecraft:smooth_basalt",
+ SoulTorch => "minecraft:soul_torch",
+ Glowstone => "minecraft:glowstone",
+ InfestedStone => "minecraft:infested_stone",
+ InfestedCobblestone => "minecraft:infested_cobblestone",
+ InfestedStoneBricks => "minecraft:infested_stone_bricks",
+ InfestedMossyStoneBricks => "minecraft:infested_mossy_stone_bricks",
+ InfestedCrackedStoneBricks => "minecraft:infested_cracked_stone_bricks",
+ InfestedChiseledStoneBricks => "minecraft:infested_chiseled_stone_bricks",
+ InfestedDeepslate => "minecraft:infested_deepslate",
+ StoneBricks => "minecraft:stone_bricks",
+ MossyStoneBricks => "minecraft:mossy_stone_bricks",
+ CrackedStoneBricks => "minecraft:cracked_stone_bricks",
+ ChiseledStoneBricks => "minecraft:chiseled_stone_bricks",
+ PackedMud => "minecraft:packed_mud",
+ MudBricks => "minecraft:mud_bricks",
+ DeepslateBricks => "minecraft:deepslate_bricks",
+ CrackedDeepslateBricks => "minecraft:cracked_deepslate_bricks",
+ DeepslateTiles => "minecraft:deepslate_tiles",
+ CrackedDeepslateTiles => "minecraft:cracked_deepslate_tiles",
+ ChiseledDeepslate => "minecraft:chiseled_deepslate",
+ ReinforcedDeepslate => "minecraft:reinforced_deepslate",
+ BrownMushroomBlock => "minecraft:brown_mushroom_block",
+ RedMushroomBlock => "minecraft:red_mushroom_block",
+ MushroomStem => "minecraft:mushroom_stem",
+ IronBars => "minecraft:iron_bars",
+ Chain => "minecraft:chain",
+ GlassPane => "minecraft:glass_pane",
+ Melon => "minecraft:melon",
+ Vine => "minecraft:vine",
+ GlowLichen => "minecraft:glow_lichen",
+ BrickStairs => "minecraft:brick_stairs",
+ StoneBrickStairs => "minecraft:stone_brick_stairs",
+ MudBrickStairs => "minecraft:mud_brick_stairs",
+ Mycelium => "minecraft:mycelium",
+ LilyPad => "minecraft:lily_pad",
+ NetherBricks => "minecraft:nether_bricks",
+ CrackedNetherBricks => "minecraft:cracked_nether_bricks",
+ ChiseledNetherBricks => "minecraft:chiseled_nether_bricks",
+ NetherBrickFence => "minecraft:nether_brick_fence",
+ NetherBrickStairs => "minecraft:nether_brick_stairs",
+ Sculk => "minecraft:sculk",
+ SculkVein => "minecraft:sculk_vein",
+ SculkCatalyst => "minecraft:sculk_catalyst",
+ SculkShrieker => "minecraft:sculk_shrieker",
+ EnchantingTable => "minecraft:enchanting_table",
+ EndPortalFrame => "minecraft:end_portal_frame",
+ EndStone => "minecraft:end_stone",
+ EndStoneBricks => "minecraft:end_stone_bricks",
+ DragonEgg => "minecraft:dragon_egg",
+ SandstoneStairs => "minecraft:sandstone_stairs",
+ EnderChest => "minecraft:ender_chest",
+ EmeraldBlock => "minecraft:emerald_block",
+ OakStairs => "minecraft:oak_stairs",
+ SpruceStairs => "minecraft:spruce_stairs",
+ BirchStairs => "minecraft:birch_stairs",
+ JungleStairs => "minecraft:jungle_stairs",
+ AcaciaStairs => "minecraft:acacia_stairs",
+ CherryStairs => "minecraft:cherry_stairs",
+ DarkOakStairs => "minecraft:dark_oak_stairs",
+ MangroveStairs => "minecraft:mangrove_stairs",
+ BambooStairs => "minecraft:bamboo_stairs",
+ BambooMosaicStairs => "minecraft:bamboo_mosaic_stairs",
+ CrimsonStairs => "minecraft:crimson_stairs",
+ WarpedStairs => "minecraft:warped_stairs",
+ CommandBlock => "minecraft:command_block",
+ Beacon => "minecraft:beacon",
+ CobblestoneWall => "minecraft:cobblestone_wall",
+ MossyCobblestoneWall => "minecraft:mossy_cobblestone_wall",
+ BrickWall => "minecraft:brick_wall",
+ PrismarineWall => "minecraft:prismarine_wall",
+ RedSandstoneWall => "minecraft:red_sandstone_wall",
+ MossyStoneBrickWall => "minecraft:mossy_stone_brick_wall",
+ GraniteWall => "minecraft:granite_wall",
+ StoneBrickWall => "minecraft:stone_brick_wall",
+ MudBrickWall => "minecraft:mud_brick_wall",
+ NetherBrickWall => "minecraft:nether_brick_wall",
+ AndesiteWall => "minecraft:andesite_wall",
+ RedNetherBrickWall => "minecraft:red_nether_brick_wall",
+ SandstoneWall => "minecraft:sandstone_wall",
+ EndStoneBrickWall => "minecraft:end_stone_brick_wall",
+ DioriteWall => "minecraft:diorite_wall",
+ BlackstoneWall => "minecraft:blackstone_wall",
+ PolishedBlackstoneWall => "minecraft:polished_blackstone_wall",
+ PolishedBlackstoneBrickWall => "minecraft:polished_blackstone_brick_wall",
+ CobbledDeepslateWall => "minecraft:cobbled_deepslate_wall",
+ PolishedDeepslateWall => "minecraft:polished_deepslate_wall",
+ DeepslateBrickWall => "minecraft:deepslate_brick_wall",
+ DeepslateTileWall => "minecraft:deepslate_tile_wall",
+ Anvil => "minecraft:anvil",
+ ChippedAnvil => "minecraft:chipped_anvil",
+ DamagedAnvil => "minecraft:damaged_anvil",
+ ChiseledQuartzBlock => "minecraft:chiseled_quartz_block",
+ QuartzBlock => "minecraft:quartz_block",
+ QuartzBricks => "minecraft:quartz_bricks",
+ QuartzPillar => "minecraft:quartz_pillar",
+ QuartzStairs => "minecraft:quartz_stairs",
+ WhiteTerracotta => "minecraft:white_terracotta",
+ OrangeTerracotta => "minecraft:orange_terracotta",
+ MagentaTerracotta => "minecraft:magenta_terracotta",
+ LightBlueTerracotta => "minecraft:light_blue_terracotta",
+ YellowTerracotta => "minecraft:yellow_terracotta",
+ LimeTerracotta => "minecraft:lime_terracotta",
+ PinkTerracotta => "minecraft:pink_terracotta",
+ GrayTerracotta => "minecraft:gray_terracotta",
+ LightGrayTerracotta => "minecraft:light_gray_terracotta",
+ CyanTerracotta => "minecraft:cyan_terracotta",
+ PurpleTerracotta => "minecraft:purple_terracotta",
+ BlueTerracotta => "minecraft:blue_terracotta",
+ BrownTerracotta => "minecraft:brown_terracotta",
+ GreenTerracotta => "minecraft:green_terracotta",
+ RedTerracotta => "minecraft:red_terracotta",
+ BlackTerracotta => "minecraft:black_terracotta",
+ Barrier => "minecraft:barrier",
+ Light => "minecraft:light",
+ HayBlock => "minecraft:hay_block",
+ WhiteCarpet => "minecraft:white_carpet",
+ OrangeCarpet => "minecraft:orange_carpet",
+ MagentaCarpet => "minecraft:magenta_carpet",
+ LightBlueCarpet => "minecraft:light_blue_carpet",
+ YellowCarpet => "minecraft:yellow_carpet",
+ LimeCarpet => "minecraft:lime_carpet",
+ PinkCarpet => "minecraft:pink_carpet",
+ GrayCarpet => "minecraft:gray_carpet",
+ LightGrayCarpet => "minecraft:light_gray_carpet",
+ CyanCarpet => "minecraft:cyan_carpet",
+ PurpleCarpet => "minecraft:purple_carpet",
+ BlueCarpet => "minecraft:blue_carpet",
+ BrownCarpet => "minecraft:brown_carpet",
+ GreenCarpet => "minecraft:green_carpet",
+ RedCarpet => "minecraft:red_carpet",
+ BlackCarpet => "minecraft:black_carpet",
+ Terracotta => "minecraft:terracotta",
+ PackedIce => "minecraft:packed_ice",
+ DirtPath => "minecraft:dirt_path",
+ Sunflower => "minecraft:sunflower",
+ Lilac => "minecraft:lilac",
+ RoseBush => "minecraft:rose_bush",
+ Peony => "minecraft:peony",
+ TallGrass => "minecraft:tall_grass",
+ LargeFern => "minecraft:large_fern",
+ WhiteStainedGlass => "minecraft:white_stained_glass",
+ OrangeStainedGlass => "minecraft:orange_stained_glass",
+ MagentaStainedGlass => "minecraft:magenta_stained_glass",
+ LightBlueStainedGlass => "minecraft:light_blue_stained_glass",
+ YellowStainedGlass => "minecraft:yellow_stained_glass",
+ LimeStainedGlass => "minecraft:lime_stained_glass",
+ PinkStainedGlass => "minecraft:pink_stained_glass",
+ GrayStainedGlass => "minecraft:gray_stained_glass",
+ LightGrayStainedGlass => "minecraft:light_gray_stained_glass",
+ CyanStainedGlass => "minecraft:cyan_stained_glass",
+ PurpleStainedGlass => "minecraft:purple_stained_glass",
+ BlueStainedGlass => "minecraft:blue_stained_glass",
+ BrownStainedGlass => "minecraft:brown_stained_glass",
+ GreenStainedGlass => "minecraft:green_stained_glass",
+ RedStainedGlass => "minecraft:red_stained_glass",
+ BlackStainedGlass => "minecraft:black_stained_glass",
+ WhiteStainedGlassPane => "minecraft:white_stained_glass_pane",
+ OrangeStainedGlassPane => "minecraft:orange_stained_glass_pane",
+ MagentaStainedGlassPane => "minecraft:magenta_stained_glass_pane",
+ LightBlueStainedGlassPane => "minecraft:light_blue_stained_glass_pane",
+ YellowStainedGlassPane => "minecraft:yellow_stained_glass_pane",
+ LimeStainedGlassPane => "minecraft:lime_stained_glass_pane",
+ PinkStainedGlassPane => "minecraft:pink_stained_glass_pane",
+ GrayStainedGlassPane => "minecraft:gray_stained_glass_pane",
+ LightGrayStainedGlassPane => "minecraft:light_gray_stained_glass_pane",
+ CyanStainedGlassPane => "minecraft:cyan_stained_glass_pane",
+ PurpleStainedGlassPane => "minecraft:purple_stained_glass_pane",
+ BlueStainedGlassPane => "minecraft:blue_stained_glass_pane",
+ BrownStainedGlassPane => "minecraft:brown_stained_glass_pane",
+ GreenStainedGlassPane => "minecraft:green_stained_glass_pane",
+ RedStainedGlassPane => "minecraft:red_stained_glass_pane",
+ BlackStainedGlassPane => "minecraft:black_stained_glass_pane",
+ Prismarine => "minecraft:prismarine",
+ PrismarineBricks => "minecraft:prismarine_bricks",
+ DarkPrismarine => "minecraft:dark_prismarine",
+ PrismarineStairs => "minecraft:prismarine_stairs",
+ PrismarineBrickStairs => "minecraft:prismarine_brick_stairs",
+ DarkPrismarineStairs => "minecraft:dark_prismarine_stairs",
+ SeaLantern => "minecraft:sea_lantern",
+ RedSandstone => "minecraft:red_sandstone",
+ ChiseledRedSandstone => "minecraft:chiseled_red_sandstone",
+ CutRedSandstone => "minecraft:cut_red_sandstone",
+ RedSandstoneStairs => "minecraft:red_sandstone_stairs",
+ RepeatingCommandBlock => "minecraft:repeating_command_block",
+ ChainCommandBlock => "minecraft:chain_command_block",
+ MagmaBlock => "minecraft:magma_block",
+ NetherWartBlock => "minecraft:nether_wart_block",
+ WarpedWartBlock => "minecraft:warped_wart_block",
+ RedNetherBricks => "minecraft:red_nether_bricks",
+ BoneBlock => "minecraft:bone_block",
+ StructureVoid => "minecraft:structure_void",
+ ShulkerBox => "minecraft:shulker_box",
+ WhiteShulkerBox => "minecraft:white_shulker_box",
+ OrangeShulkerBox => "minecraft:orange_shulker_box",
+ MagentaShulkerBox => "minecraft:magenta_shulker_box",
+ LightBlueShulkerBox => "minecraft:light_blue_shulker_box",
+ YellowShulkerBox => "minecraft:yellow_shulker_box",
+ LimeShulkerBox => "minecraft:lime_shulker_box",
+ PinkShulkerBox => "minecraft:pink_shulker_box",
+ GrayShulkerBox => "minecraft:gray_shulker_box",
+ LightGrayShulkerBox => "minecraft:light_gray_shulker_box",
+ CyanShulkerBox => "minecraft:cyan_shulker_box",
+ PurpleShulkerBox => "minecraft:purple_shulker_box",
+ BlueShulkerBox => "minecraft:blue_shulker_box",
+ BrownShulkerBox => "minecraft:brown_shulker_box",
+ GreenShulkerBox => "minecraft:green_shulker_box",
+ RedShulkerBox => "minecraft:red_shulker_box",
+ BlackShulkerBox => "minecraft:black_shulker_box",
+ WhiteGlazedTerracotta => "minecraft:white_glazed_terracotta",
+ OrangeGlazedTerracotta => "minecraft:orange_glazed_terracotta",
+ MagentaGlazedTerracotta => "minecraft:magenta_glazed_terracotta",
+ LightBlueGlazedTerracotta => "minecraft:light_blue_glazed_terracotta",
+ YellowGlazedTerracotta => "minecraft:yellow_glazed_terracotta",
+ LimeGlazedTerracotta => "minecraft:lime_glazed_terracotta",
+ PinkGlazedTerracotta => "minecraft:pink_glazed_terracotta",
+ GrayGlazedTerracotta => "minecraft:gray_glazed_terracotta",
+ LightGrayGlazedTerracotta => "minecraft:light_gray_glazed_terracotta",
+ CyanGlazedTerracotta => "minecraft:cyan_glazed_terracotta",
+ PurpleGlazedTerracotta => "minecraft:purple_glazed_terracotta",
+ BlueGlazedTerracotta => "minecraft:blue_glazed_terracotta",
+ BrownGlazedTerracotta => "minecraft:brown_glazed_terracotta",
+ GreenGlazedTerracotta => "minecraft:green_glazed_terracotta",
+ RedGlazedTerracotta => "minecraft:red_glazed_terracotta",
+ BlackGlazedTerracotta => "minecraft:black_glazed_terracotta",
+ WhiteConcrete => "minecraft:white_concrete",
+ OrangeConcrete => "minecraft:orange_concrete",
+ MagentaConcrete => "minecraft:magenta_concrete",
+ LightBlueConcrete => "minecraft:light_blue_concrete",
+ YellowConcrete => "minecraft:yellow_concrete",
+ LimeConcrete => "minecraft:lime_concrete",
+ PinkConcrete => "minecraft:pink_concrete",
+ GrayConcrete => "minecraft:gray_concrete",
+ LightGrayConcrete => "minecraft:light_gray_concrete",
+ CyanConcrete => "minecraft:cyan_concrete",
+ PurpleConcrete => "minecraft:purple_concrete",
+ BlueConcrete => "minecraft:blue_concrete",
+ BrownConcrete => "minecraft:brown_concrete",
+ GreenConcrete => "minecraft:green_concrete",
+ RedConcrete => "minecraft:red_concrete",
+ BlackConcrete => "minecraft:black_concrete",
+ WhiteConcretePowder => "minecraft:white_concrete_powder",
+ OrangeConcretePowder => "minecraft:orange_concrete_powder",
+ MagentaConcretePowder => "minecraft:magenta_concrete_powder",
+ LightBlueConcretePowder => "minecraft:light_blue_concrete_powder",
+ YellowConcretePowder => "minecraft:yellow_concrete_powder",
+ LimeConcretePowder => "minecraft:lime_concrete_powder",
+ PinkConcretePowder => "minecraft:pink_concrete_powder",
+ GrayConcretePowder => "minecraft:gray_concrete_powder",
+ LightGrayConcretePowder => "minecraft:light_gray_concrete_powder",
+ CyanConcretePowder => "minecraft:cyan_concrete_powder",
+ PurpleConcretePowder => "minecraft:purple_concrete_powder",
+ BlueConcretePowder => "minecraft:blue_concrete_powder",
+ BrownConcretePowder => "minecraft:brown_concrete_powder",
+ GreenConcretePowder => "minecraft:green_concrete_powder",
+ RedConcretePowder => "minecraft:red_concrete_powder",
+ BlackConcretePowder => "minecraft:black_concrete_powder",
+ TurtleEgg => "minecraft:turtle_egg",
+ DeadTubeCoralBlock => "minecraft:dead_tube_coral_block",
+ DeadBrainCoralBlock => "minecraft:dead_brain_coral_block",
+ DeadBubbleCoralBlock => "minecraft:dead_bubble_coral_block",
+ DeadFireCoralBlock => "minecraft:dead_fire_coral_block",
+ DeadHornCoralBlock => "minecraft:dead_horn_coral_block",
+ TubeCoralBlock => "minecraft:tube_coral_block",
+ BrainCoralBlock => "minecraft:brain_coral_block",
+ BubbleCoralBlock => "minecraft:bubble_coral_block",
+ FireCoralBlock => "minecraft:fire_coral_block",
+ HornCoralBlock => "minecraft:horn_coral_block",
+ TubeCoral => "minecraft:tube_coral",
+ BrainCoral => "minecraft:brain_coral",
+ BubbleCoral => "minecraft:bubble_coral",
+ FireCoral => "minecraft:fire_coral",
+ HornCoral => "minecraft:horn_coral",
+ DeadBrainCoral => "minecraft:dead_brain_coral",
+ DeadBubbleCoral => "minecraft:dead_bubble_coral",
+ DeadFireCoral => "minecraft:dead_fire_coral",
+ DeadHornCoral => "minecraft:dead_horn_coral",
+ DeadTubeCoral => "minecraft:dead_tube_coral",
+ TubeCoralFan => "minecraft:tube_coral_fan",
+ BrainCoralFan => "minecraft:brain_coral_fan",
+ BubbleCoralFan => "minecraft:bubble_coral_fan",
+ FireCoralFan => "minecraft:fire_coral_fan",
+ HornCoralFan => "minecraft:horn_coral_fan",
+ DeadTubeCoralFan => "minecraft:dead_tube_coral_fan",
+ DeadBrainCoralFan => "minecraft:dead_brain_coral_fan",
+ DeadBubbleCoralFan => "minecraft:dead_bubble_coral_fan",
+ DeadFireCoralFan => "minecraft:dead_fire_coral_fan",
+ DeadHornCoralFan => "minecraft:dead_horn_coral_fan",
+ BlueIce => "minecraft:blue_ice",
+ Conduit => "minecraft:conduit",
+ PolishedGraniteStairs => "minecraft:polished_granite_stairs",
+ SmoothRedSandstoneStairs => "minecraft:smooth_red_sandstone_stairs",
+ MossyStoneBrickStairs => "minecraft:mossy_stone_brick_stairs",
+ PolishedDioriteStairs => "minecraft:polished_diorite_stairs",
+ MossyCobblestoneStairs => "minecraft:mossy_cobblestone_stairs",
+ EndStoneBrickStairs => "minecraft:end_stone_brick_stairs",
+ StoneStairs => "minecraft:stone_stairs",
+ SmoothSandstoneStairs => "minecraft:smooth_sandstone_stairs",
+ SmoothQuartzStairs => "minecraft:smooth_quartz_stairs",
+ GraniteStairs => "minecraft:granite_stairs",
+ AndesiteStairs => "minecraft:andesite_stairs",
+ RedNetherBrickStairs => "minecraft:red_nether_brick_stairs",
+ PolishedAndesiteStairs => "minecraft:polished_andesite_stairs",
+ DioriteStairs => "minecraft:diorite_stairs",
+ CobbledDeepslateStairs => "minecraft:cobbled_deepslate_stairs",
+ PolishedDeepslateStairs => "minecraft:polished_deepslate_stairs",
+ DeepslateBrickStairs => "minecraft:deepslate_brick_stairs",
+ DeepslateTileStairs => "minecraft:deepslate_tile_stairs",
+ PolishedGraniteSlab => "minecraft:polished_granite_slab",
+ SmoothRedSandstoneSlab => "minecraft:smooth_red_sandstone_slab",
+ MossyStoneBrickSlab => "minecraft:mossy_stone_brick_slab",
+ PolishedDioriteSlab => "minecraft:polished_diorite_slab",
+ MossyCobblestoneSlab => "minecraft:mossy_cobblestone_slab",
+ EndStoneBrickSlab => "minecraft:end_stone_brick_slab",
+ SmoothSandstoneSlab => "minecraft:smooth_sandstone_slab",
+ SmoothQuartzSlab => "minecraft:smooth_quartz_slab",
+ GraniteSlab => "minecraft:granite_slab",
+ AndesiteSlab => "minecraft:andesite_slab",
+ RedNetherBrickSlab => "minecraft:red_nether_brick_slab",
+ PolishedAndesiteSlab => "minecraft:polished_andesite_slab",
+ DioriteSlab => "minecraft:diorite_slab",
+ CobbledDeepslateSlab => "minecraft:cobbled_deepslate_slab",
+ PolishedDeepslateSlab => "minecraft:polished_deepslate_slab",
+ DeepslateBrickSlab => "minecraft:deepslate_brick_slab",
+ DeepslateTileSlab => "minecraft:deepslate_tile_slab",
+ Scaffolding => "minecraft:scaffolding",
+ Redstone => "minecraft:redstone",
+ RedstoneTorch => "minecraft:redstone_torch",
+ RedstoneBlock => "minecraft:redstone_block",
+ Repeater => "minecraft:repeater",
+ Comparator => "minecraft:comparator",
+ Piston => "minecraft:piston",
+ StickyPiston => "minecraft:sticky_piston",
+ SlimeBlock => "minecraft:slime_block",
+ HoneyBlock => "minecraft:honey_block",
+ Observer => "minecraft:observer",
+ Hopper => "minecraft:hopper",
+ Dispenser => "minecraft:dispenser",
+ Dropper => "minecraft:dropper",
+ Lectern => "minecraft:lectern",
+ Target => "minecraft:target",
+ Lever => "minecraft:lever",
+ LightningRod => "minecraft:lightning_rod",
+ DaylightDetector => "minecraft:daylight_detector",
+ SculkSensor => "minecraft:sculk_sensor",
+ TripwireHook => "minecraft:tripwire_hook",
+ TrappedChest => "minecraft:trapped_chest",
+ Tnt => "minecraft:tnt",
+ RedstoneLamp => "minecraft:redstone_lamp",
+ NoteBlock => "minecraft:note_block",
+ StoneButton => "minecraft:stone_button",
+ PolishedBlackstoneButton => "minecraft:polished_blackstone_button",
+ OakButton => "minecraft:oak_button",
+ SpruceButton => "minecraft:spruce_button",
+ BirchButton => "minecraft:birch_button",
+ JungleButton => "minecraft:jungle_button",
+ AcaciaButton => "minecraft:acacia_button",
+ CherryButton => "minecraft:cherry_button",
+ DarkOakButton => "minecraft:dark_oak_button",
+ MangroveButton => "minecraft:mangrove_button",
+ BambooButton => "minecraft:bamboo_button",
+ CrimsonButton => "minecraft:crimson_button",
+ WarpedButton => "minecraft:warped_button",
+ StonePressurePlate => "minecraft:stone_pressure_plate",
+ PolishedBlackstonePressurePlate => "minecraft:polished_blackstone_pressure_plate",
+ LightWeightedPressurePlate => "minecraft:light_weighted_pressure_plate",
+ HeavyWeightedPressurePlate => "minecraft:heavy_weighted_pressure_plate",
+ OakPressurePlate => "minecraft:oak_pressure_plate",
+ SprucePressurePlate => "minecraft:spruce_pressure_plate",
+ BirchPressurePlate => "minecraft:birch_pressure_plate",
+ JunglePressurePlate => "minecraft:jungle_pressure_plate",
+ AcaciaPressurePlate => "minecraft:acacia_pressure_plate",
+ CherryPressurePlate => "minecraft:cherry_pressure_plate",
+ DarkOakPressurePlate => "minecraft:dark_oak_pressure_plate",
+ MangrovePressurePlate => "minecraft:mangrove_pressure_plate",
+ BambooPressurePlate => "minecraft:bamboo_pressure_plate",
+ CrimsonPressurePlate => "minecraft:crimson_pressure_plate",
+ WarpedPressurePlate => "minecraft:warped_pressure_plate",
+ IronDoor => "minecraft:iron_door",
+ OakDoor => "minecraft:oak_door",
+ SpruceDoor => "minecraft:spruce_door",
+ BirchDoor => "minecraft:birch_door",
+ JungleDoor => "minecraft:jungle_door",
+ AcaciaDoor => "minecraft:acacia_door",
+ CherryDoor => "minecraft:cherry_door",
+ DarkOakDoor => "minecraft:dark_oak_door",
+ MangroveDoor => "minecraft:mangrove_door",
+ BambooDoor => "minecraft:bamboo_door",
+ CrimsonDoor => "minecraft:crimson_door",
+ WarpedDoor => "minecraft:warped_door",
+ IronTrapdoor => "minecraft:iron_trapdoor",
+ OakTrapdoor => "minecraft:oak_trapdoor",
+ SpruceTrapdoor => "minecraft:spruce_trapdoor",
+ BirchTrapdoor => "minecraft:birch_trapdoor",
+ JungleTrapdoor => "minecraft:jungle_trapdoor",
+ AcaciaTrapdoor => "minecraft:acacia_trapdoor",
+ CherryTrapdoor => "minecraft:cherry_trapdoor",
+ DarkOakTrapdoor => "minecraft:dark_oak_trapdoor",
+ MangroveTrapdoor => "minecraft:mangrove_trapdoor",
+ BambooTrapdoor => "minecraft:bamboo_trapdoor",
+ CrimsonTrapdoor => "minecraft:crimson_trapdoor",
+ WarpedTrapdoor => "minecraft:warped_trapdoor",
+ OakFenceGate => "minecraft:oak_fence_gate",
+ SpruceFenceGate => "minecraft:spruce_fence_gate",
+ BirchFenceGate => "minecraft:birch_fence_gate",
+ JungleFenceGate => "minecraft:jungle_fence_gate",
+ AcaciaFenceGate => "minecraft:acacia_fence_gate",
+ CherryFenceGate => "minecraft:cherry_fence_gate",
+ DarkOakFenceGate => "minecraft:dark_oak_fence_gate",
+ MangroveFenceGate => "minecraft:mangrove_fence_gate",
+ BambooFenceGate => "minecraft:bamboo_fence_gate",
+ CrimsonFenceGate => "minecraft:crimson_fence_gate",
+ WarpedFenceGate => "minecraft:warped_fence_gate",
+ PoweredRail => "minecraft:powered_rail",
+ DetectorRail => "minecraft:detector_rail",
+ Rail => "minecraft:rail",
+ ActivatorRail => "minecraft:activator_rail",
+ Saddle => "minecraft:saddle",
+ Minecart => "minecraft:minecart",
+ ChestMinecart => "minecraft:chest_minecart",
+ FurnaceMinecart => "minecraft:furnace_minecart",
+ TntMinecart => "minecraft:tnt_minecart",
+ HopperMinecart => "minecraft:hopper_minecart",
+ CarrotOnAStick => "minecraft:carrot_on_a_stick",
+ WarpedFungusOnAStick => "minecraft:warped_fungus_on_a_stick",
+ Elytra => "minecraft:elytra",
+ OakBoat => "minecraft:oak_boat",
+ OakChestBoat => "minecraft:oak_chest_boat",
+ SpruceBoat => "minecraft:spruce_boat",
+ SpruceChestBoat => "minecraft:spruce_chest_boat",
+ BirchBoat => "minecraft:birch_boat",
+ BirchChestBoat => "minecraft:birch_chest_boat",
+ JungleBoat => "minecraft:jungle_boat",
+ JungleChestBoat => "minecraft:jungle_chest_boat",
+ AcaciaBoat => "minecraft:acacia_boat",
+ AcaciaChestBoat => "minecraft:acacia_chest_boat",
+ CherryBoat => "minecraft:cherry_boat",
+ CherryChestBoat => "minecraft:cherry_chest_boat",
+ DarkOakBoat => "minecraft:dark_oak_boat",
+ DarkOakChestBoat => "minecraft:dark_oak_chest_boat",
+ MangroveBoat => "minecraft:mangrove_boat",
+ MangroveChestBoat => "minecraft:mangrove_chest_boat",
+ BambooRaft => "minecraft:bamboo_raft",
+ BambooChestRaft => "minecraft:bamboo_chest_raft",
+ StructureBlock => "minecraft:structure_block",
+ Jigsaw => "minecraft:jigsaw",
+ TurtleHelmet => "minecraft:turtle_helmet",
+ Scute => "minecraft:scute",
+ FlintAndSteel => "minecraft:flint_and_steel",
+ Apple => "minecraft:apple",
+ Bow => "minecraft:bow",
+ Arrow => "minecraft:arrow",
+ Coal => "minecraft:coal",
+ Charcoal => "minecraft:charcoal",
+ Diamond => "minecraft:diamond",
+ Emerald => "minecraft:emerald",
+ LapisLazuli => "minecraft:lapis_lazuli",
+ Quartz => "minecraft:quartz",
+ AmethystShard => "minecraft:amethyst_shard",
+ RawIron => "minecraft:raw_iron",
+ IronIngot => "minecraft:iron_ingot",
+ RawCopper => "minecraft:raw_copper",
+ CopperIngot => "minecraft:copper_ingot",
+ RawGold => "minecraft:raw_gold",
+ GoldIngot => "minecraft:gold_ingot",
+ NetheriteIngot => "minecraft:netherite_ingot",
+ NetheriteScrap => "minecraft:netherite_scrap",
+ WoodenSword => "minecraft:wooden_sword",
+ WoodenShovel => "minecraft:wooden_shovel",
+ WoodenPickaxe => "minecraft:wooden_pickaxe",
+ WoodenAxe => "minecraft:wooden_axe",
+ WoodenHoe => "minecraft:wooden_hoe",
+ StoneSword => "minecraft:stone_sword",
+ StoneShovel => "minecraft:stone_shovel",
+ StonePickaxe => "minecraft:stone_pickaxe",
+ StoneAxe => "minecraft:stone_axe",
+ StoneHoe => "minecraft:stone_hoe",
+ GoldenSword => "minecraft:golden_sword",
+ GoldenShovel => "minecraft:golden_shovel",
+ GoldenPickaxe => "minecraft:golden_pickaxe",
+ GoldenAxe => "minecraft:golden_axe",
+ GoldenHoe => "minecraft:golden_hoe",
+ IronSword => "minecraft:iron_sword",
+ IronShovel => "minecraft:iron_shovel",
+ IronPickaxe => "minecraft:iron_pickaxe",
+ IronAxe => "minecraft:iron_axe",
+ IronHoe => "minecraft:iron_hoe",
+ DiamondSword => "minecraft:diamond_sword",
+ DiamondShovel => "minecraft:diamond_shovel",
+ DiamondPickaxe => "minecraft:diamond_pickaxe",
+ DiamondAxe => "minecraft:diamond_axe",
+ DiamondHoe => "minecraft:diamond_hoe",
+ NetheriteSword => "minecraft:netherite_sword",
+ NetheriteShovel => "minecraft:netherite_shovel",
+ NetheritePickaxe => "minecraft:netherite_pickaxe",
+ NetheriteAxe => "minecraft:netherite_axe",
+ NetheriteHoe => "minecraft:netherite_hoe",
+ Stick => "minecraft:stick",
+ Bowl => "minecraft:bowl",
+ MushroomStew => "minecraft:mushroom_stew",
+ String => "minecraft:string",
+ Feather => "minecraft:feather",
+ Gunpowder => "minecraft:gunpowder",
+ WheatSeeds => "minecraft:wheat_seeds",
+ Wheat => "minecraft:wheat",
+ Bread => "minecraft:bread",
+ LeatherHelmet => "minecraft:leather_helmet",
+ LeatherChestplate => "minecraft:leather_chestplate",
+ LeatherLeggings => "minecraft:leather_leggings",
+ LeatherBoots => "minecraft:leather_boots",
+ ChainmailHelmet => "minecraft:chainmail_helmet",
+ ChainmailChestplate => "minecraft:chainmail_chestplate",
+ ChainmailLeggings => "minecraft:chainmail_leggings",
+ ChainmailBoots => "minecraft:chainmail_boots",
+ IronHelmet => "minecraft:iron_helmet",
+ IronChestplate => "minecraft:iron_chestplate",
+ IronLeggings => "minecraft:iron_leggings",
+ IronBoots => "minecraft:iron_boots",
+ DiamondHelmet => "minecraft:diamond_helmet",
+ DiamondChestplate => "minecraft:diamond_chestplate",
+ DiamondLeggings => "minecraft:diamond_leggings",
+ DiamondBoots => "minecraft:diamond_boots",
+ GoldenHelmet => "minecraft:golden_helmet",
+ GoldenChestplate => "minecraft:golden_chestplate",
+ GoldenLeggings => "minecraft:golden_leggings",
+ GoldenBoots => "minecraft:golden_boots",
+ NetheriteHelmet => "minecraft:netherite_helmet",
+ NetheriteChestplate => "minecraft:netherite_chestplate",
+ NetheriteLeggings => "minecraft:netherite_leggings",
+ NetheriteBoots => "minecraft:netherite_boots",
+ Flint => "minecraft:flint",
+ Porkchop => "minecraft:porkchop",
+ CookedPorkchop => "minecraft:cooked_porkchop",
+ Painting => "minecraft:painting",
+ GoldenApple => "minecraft:golden_apple",
+ EnchantedGoldenApple => "minecraft:enchanted_golden_apple",
+ OakSign => "minecraft:oak_sign",
+ SpruceSign => "minecraft:spruce_sign",
+ BirchSign => "minecraft:birch_sign",
+ JungleSign => "minecraft:jungle_sign",
+ AcaciaSign => "minecraft:acacia_sign",
+ CherrySign => "minecraft:cherry_sign",
+ DarkOakSign => "minecraft:dark_oak_sign",
+ MangroveSign => "minecraft:mangrove_sign",
+ BambooSign => "minecraft:bamboo_sign",
+ CrimsonSign => "minecraft:crimson_sign",
+ WarpedSign => "minecraft:warped_sign",
+ OakHangingSign => "minecraft:oak_hanging_sign",
+ SpruceHangingSign => "minecraft:spruce_hanging_sign",
+ BirchHangingSign => "minecraft:birch_hanging_sign",
+ JungleHangingSign => "minecraft:jungle_hanging_sign",
+ AcaciaHangingSign => "minecraft:acacia_hanging_sign",
+ CherryHangingSign => "minecraft:cherry_hanging_sign",
+ DarkOakHangingSign => "minecraft:dark_oak_hanging_sign",
+ MangroveHangingSign => "minecraft:mangrove_hanging_sign",
+ BambooHangingSign => "minecraft:bamboo_hanging_sign",
+ CrimsonHangingSign => "minecraft:crimson_hanging_sign",
+ WarpedHangingSign => "minecraft:warped_hanging_sign",
+ Bucket => "minecraft:bucket",
+ WaterBucket => "minecraft:water_bucket",
+ LavaBucket => "minecraft:lava_bucket",
+ PowderSnowBucket => "minecraft:powder_snow_bucket",
+ Snowball => "minecraft:snowball",
+ Leather => "minecraft:leather",
+ MilkBucket => "minecraft:milk_bucket",
+ PufferfishBucket => "minecraft:pufferfish_bucket",
+ SalmonBucket => "minecraft:salmon_bucket",
+ CodBucket => "minecraft:cod_bucket",
+ TropicalFishBucket => "minecraft:tropical_fish_bucket",
+ AxolotlBucket => "minecraft:axolotl_bucket",
+ TadpoleBucket => "minecraft:tadpole_bucket",
+ Brick => "minecraft:brick",
+ ClayBall => "minecraft:clay_ball",
+ DriedKelpBlock => "minecraft:dried_kelp_block",
+ Paper => "minecraft:paper",
+ Book => "minecraft:book",
+ SlimeBall => "minecraft:slime_ball",
+ Egg => "minecraft:egg",
+ Compass => "minecraft:compass",
+ RecoveryCompass => "minecraft:recovery_compass",
+ Bundle => "minecraft:bundle",
+ FishingRod => "minecraft:fishing_rod",
+ Clock => "minecraft:clock",
+ Spyglass => "minecraft:spyglass",
+ GlowstoneDust => "minecraft:glowstone_dust",
+ Cod => "minecraft:cod",
+ Salmon => "minecraft:salmon",
+ TropicalFish => "minecraft:tropical_fish",
+ Pufferfish => "minecraft:pufferfish",
+ CookedCod => "minecraft:cooked_cod",
+ CookedSalmon => "minecraft:cooked_salmon",
+ InkSac => "minecraft:ink_sac",
+ GlowInkSac => "minecraft:glow_ink_sac",
+ CocoaBeans => "minecraft:cocoa_beans",
+ WhiteDye => "minecraft:white_dye",
+ OrangeDye => "minecraft:orange_dye",
+ MagentaDye => "minecraft:magenta_dye",
+ LightBlueDye => "minecraft:light_blue_dye",
+ YellowDye => "minecraft:yellow_dye",
+ LimeDye => "minecraft:lime_dye",
+ PinkDye => "minecraft:pink_dye",
+ GrayDye => "minecraft:gray_dye",
+ LightGrayDye => "minecraft:light_gray_dye",
+ CyanDye => "minecraft:cyan_dye",
+ PurpleDye => "minecraft:purple_dye",
+ BlueDye => "minecraft:blue_dye",
+ BrownDye => "minecraft:brown_dye",
+ GreenDye => "minecraft:green_dye",
+ RedDye => "minecraft:red_dye",
+ BlackDye => "minecraft:black_dye",
+ BoneMeal => "minecraft:bone_meal",
+ Bone => "minecraft:bone",
+ Sugar => "minecraft:sugar",
+ Cake => "minecraft:cake",
+ WhiteBed => "minecraft:white_bed",
+ OrangeBed => "minecraft:orange_bed",
+ MagentaBed => "minecraft:magenta_bed",
+ LightBlueBed => "minecraft:light_blue_bed",
+ YellowBed => "minecraft:yellow_bed",
+ LimeBed => "minecraft:lime_bed",
+ PinkBed => "minecraft:pink_bed",
+ GrayBed => "minecraft:gray_bed",
+ LightGrayBed => "minecraft:light_gray_bed",
+ CyanBed => "minecraft:cyan_bed",
+ PurpleBed => "minecraft:purple_bed",
+ BlueBed => "minecraft:blue_bed",
+ BrownBed => "minecraft:brown_bed",
+ GreenBed => "minecraft:green_bed",
+ RedBed => "minecraft:red_bed",
+ BlackBed => "minecraft:black_bed",
+ Cookie => "minecraft:cookie",
+ FilledMap => "minecraft:filled_map",
+ Shears => "minecraft:shears",
+ MelonSlice => "minecraft:melon_slice",
+ DriedKelp => "minecraft:dried_kelp",
+ PumpkinSeeds => "minecraft:pumpkin_seeds",
+ MelonSeeds => "minecraft:melon_seeds",
+ Beef => "minecraft:beef",
+ CookedBeef => "minecraft:cooked_beef",
+ Chicken => "minecraft:chicken",
+ CookedChicken => "minecraft:cooked_chicken",
+ RottenFlesh => "minecraft:rotten_flesh",
+ EnderPearl => "minecraft:ender_pearl",
+ BlazeRod => "minecraft:blaze_rod",
+ GhastTear => "minecraft:ghast_tear",
+ GoldNugget => "minecraft:gold_nugget",
+ NetherWart => "minecraft:nether_wart",
+ Potion => "minecraft:potion",
+ GlassBottle => "minecraft:glass_bottle",
+ SpiderEye => "minecraft:spider_eye",
+ FermentedSpiderEye => "minecraft:fermented_spider_eye",
+ BlazePowder => "minecraft:blaze_powder",
+ MagmaCream => "minecraft:magma_cream",
+ BrewingStand => "minecraft:brewing_stand",
+ Cauldron => "minecraft:cauldron",
+ EnderEye => "minecraft:ender_eye",
+ GlisteringMelonSlice => "minecraft:glistering_melon_slice",
+ AllaySpawnEgg => "minecraft:allay_spawn_egg",
+ AxolotlSpawnEgg => "minecraft:axolotl_spawn_egg",
+ BatSpawnEgg => "minecraft:bat_spawn_egg",
+ BeeSpawnEgg => "minecraft:bee_spawn_egg",
+ BlazeSpawnEgg => "minecraft:blaze_spawn_egg",
+ CatSpawnEgg => "minecraft:cat_spawn_egg",
+ CamelSpawnEgg => "minecraft:camel_spawn_egg",
+ CaveSpiderSpawnEgg => "minecraft:cave_spider_spawn_egg",
+ ChickenSpawnEgg => "minecraft:chicken_spawn_egg",
+ CodSpawnEgg => "minecraft:cod_spawn_egg",
+ CowSpawnEgg => "minecraft:cow_spawn_egg",
+ CreeperSpawnEgg => "minecraft:creeper_spawn_egg",
+ DolphinSpawnEgg => "minecraft:dolphin_spawn_egg",
+ DonkeySpawnEgg => "minecraft:donkey_spawn_egg",
+ DrownedSpawnEgg => "minecraft:drowned_spawn_egg",
+ ElderGuardianSpawnEgg => "minecraft:elder_guardian_spawn_egg",
+ EnderDragonSpawnEgg => "minecraft:ender_dragon_spawn_egg",
+ EndermanSpawnEgg => "minecraft:enderman_spawn_egg",
+ EndermiteSpawnEgg => "minecraft:endermite_spawn_egg",
+ EvokerSpawnEgg => "minecraft:evoker_spawn_egg",
+ FoxSpawnEgg => "minecraft:fox_spawn_egg",
+ FrogSpawnEgg => "minecraft:frog_spawn_egg",
+ GhastSpawnEgg => "minecraft:ghast_spawn_egg",
+ GlowSquidSpawnEgg => "minecraft:glow_squid_spawn_egg",
+ GoatSpawnEgg => "minecraft:goat_spawn_egg",
+ GuardianSpawnEgg => "minecraft:guardian_spawn_egg",
+ HoglinSpawnEgg => "minecraft:hoglin_spawn_egg",
+ HorseSpawnEgg => "minecraft:horse_spawn_egg",
+ HuskSpawnEgg => "minecraft:husk_spawn_egg",
+ IronGolemSpawnEgg => "minecraft:iron_golem_spawn_egg",
+ LlamaSpawnEgg => "minecraft:llama_spawn_egg",
+ MagmaCubeSpawnEgg => "minecraft:magma_cube_spawn_egg",
+ MooshroomSpawnEgg => "minecraft:mooshroom_spawn_egg",
+ MuleSpawnEgg => "minecraft:mule_spawn_egg",
+ OcelotSpawnEgg => "minecraft:ocelot_spawn_egg",
+ PandaSpawnEgg => "minecraft:panda_spawn_egg",
+ ParrotSpawnEgg => "minecraft:parrot_spawn_egg",
+ PhantomSpawnEgg => "minecraft:phantom_spawn_egg",
+ PigSpawnEgg => "minecraft:pig_spawn_egg",
+ PiglinSpawnEgg => "minecraft:piglin_spawn_egg",
+ PiglinBruteSpawnEgg => "minecraft:piglin_brute_spawn_egg",
+ PillagerSpawnEgg => "minecraft:pillager_spawn_egg",
+ PolarBearSpawnEgg => "minecraft:polar_bear_spawn_egg",
+ PufferfishSpawnEgg => "minecraft:pufferfish_spawn_egg",
+ RabbitSpawnEgg => "minecraft:rabbit_spawn_egg",
+ RavagerSpawnEgg => "minecraft:ravager_spawn_egg",
+ SalmonSpawnEgg => "minecraft:salmon_spawn_egg",
+ SheepSpawnEgg => "minecraft:sheep_spawn_egg",
+ ShulkerSpawnEgg => "minecraft:shulker_spawn_egg",
+ SilverfishSpawnEgg => "minecraft:silverfish_spawn_egg",
+ SkeletonSpawnEgg => "minecraft:skeleton_spawn_egg",
+ SkeletonHorseSpawnEgg => "minecraft:skeleton_horse_spawn_egg",
+ SlimeSpawnEgg => "minecraft:slime_spawn_egg",
+ SnifferSpawnEgg => "minecraft:sniffer_spawn_egg",
+ SnowGolemSpawnEgg => "minecraft:snow_golem_spawn_egg",
+ SpiderSpawnEgg => "minecraft:spider_spawn_egg",
+ SquidSpawnEgg => "minecraft:squid_spawn_egg",
+ StraySpawnEgg => "minecraft:stray_spawn_egg",
+ StriderSpawnEgg => "minecraft:strider_spawn_egg",
+ TadpoleSpawnEgg => "minecraft:tadpole_spawn_egg",
+ TraderLlamaSpawnEgg => "minecraft:trader_llama_spawn_egg",
+ TropicalFishSpawnEgg => "minecraft:tropical_fish_spawn_egg",
+ TurtleSpawnEgg => "minecraft:turtle_spawn_egg",
+ VexSpawnEgg => "minecraft:vex_spawn_egg",
+ VillagerSpawnEgg => "minecraft:villager_spawn_egg",
+ VindicatorSpawnEgg => "minecraft:vindicator_spawn_egg",
+ WanderingTraderSpawnEgg => "minecraft:wandering_trader_spawn_egg",
+ WardenSpawnEgg => "minecraft:warden_spawn_egg",
+ WitchSpawnEgg => "minecraft:witch_spawn_egg",
+ WitherSpawnEgg => "minecraft:wither_spawn_egg",
+ WitherSkeletonSpawnEgg => "minecraft:wither_skeleton_spawn_egg",
+ WolfSpawnEgg => "minecraft:wolf_spawn_egg",
+ ZoglinSpawnEgg => "minecraft:zoglin_spawn_egg",
+ ZombieSpawnEgg => "minecraft:zombie_spawn_egg",
+ ZombieHorseSpawnEgg => "minecraft:zombie_horse_spawn_egg",
+ ZombieVillagerSpawnEgg => "minecraft:zombie_villager_spawn_egg",
+ ZombifiedPiglinSpawnEgg => "minecraft:zombified_piglin_spawn_egg",
+ ExperienceBottle => "minecraft:experience_bottle",
+ FireCharge => "minecraft:fire_charge",
+ WritableBook => "minecraft:writable_book",
+ WrittenBook => "minecraft:written_book",
+ ItemFrame => "minecraft:item_frame",
+ GlowItemFrame => "minecraft:glow_item_frame",
+ FlowerPot => "minecraft:flower_pot",
+ Carrot => "minecraft:carrot",
+ Potato => "minecraft:potato",
+ BakedPotato => "minecraft:baked_potato",
+ PoisonousPotato => "minecraft:poisonous_potato",
+ Map => "minecraft:map",
+ GoldenCarrot => "minecraft:golden_carrot",
+ SkeletonSkull => "minecraft:skeleton_skull",
+ WitherSkeletonSkull => "minecraft:wither_skeleton_skull",
+ PlayerHead => "minecraft:player_head",
+ ZombieHead => "minecraft:zombie_head",
+ CreeperHead => "minecraft:creeper_head",
+ DragonHead => "minecraft:dragon_head",
+ PiglinHead => "minecraft:piglin_head",
+ NetherStar => "minecraft:nether_star",
+ PumpkinPie => "minecraft:pumpkin_pie",
+ FireworkRocket => "minecraft:firework_rocket",
+ FireworkStar => "minecraft:firework_star",
+ EnchantedBook => "minecraft:enchanted_book",
+ NetherBrick => "minecraft:nether_brick",
+ PrismarineShard => "minecraft:prismarine_shard",
+ PrismarineCrystals => "minecraft:prismarine_crystals",
+ Rabbit => "minecraft:rabbit",
+ CookedRabbit => "minecraft:cooked_rabbit",
+ RabbitStew => "minecraft:rabbit_stew",
+ RabbitFoot => "minecraft:rabbit_foot",
+ RabbitHide => "minecraft:rabbit_hide",
+ ArmorStand => "minecraft:armor_stand",
+ IronHorseArmor => "minecraft:iron_horse_armor",
+ GoldenHorseArmor => "minecraft:golden_horse_armor",
+ DiamondHorseArmor => "minecraft:diamond_horse_armor",
+ LeatherHorseArmor => "minecraft:leather_horse_armor",
+ Lead => "minecraft:lead",
+ NameTag => "minecraft:name_tag",
+ CommandBlockMinecart => "minecraft:command_block_minecart",
+ Mutton => "minecraft:mutton",
+ CookedMutton => "minecraft:cooked_mutton",
+ WhiteBanner => "minecraft:white_banner",
+ OrangeBanner => "minecraft:orange_banner",
+ MagentaBanner => "minecraft:magenta_banner",
+ LightBlueBanner => "minecraft:light_blue_banner",
+ YellowBanner => "minecraft:yellow_banner",
+ LimeBanner => "minecraft:lime_banner",
+ PinkBanner => "minecraft:pink_banner",
+ GrayBanner => "minecraft:gray_banner",
+ LightGrayBanner => "minecraft:light_gray_banner",
+ CyanBanner => "minecraft:cyan_banner",
+ PurpleBanner => "minecraft:purple_banner",
+ BlueBanner => "minecraft:blue_banner",
+ BrownBanner => "minecraft:brown_banner",
+ GreenBanner => "minecraft:green_banner",
+ RedBanner => "minecraft:red_banner",
+ BlackBanner => "minecraft:black_banner",
+ EndCrystal => "minecraft:end_crystal",
+ ChorusFruit => "minecraft:chorus_fruit",
+ PoppedChorusFruit => "minecraft:popped_chorus_fruit",
+ TorchflowerSeeds => "minecraft:torchflower_seeds",
+ Beetroot => "minecraft:beetroot",
+ BeetrootSeeds => "minecraft:beetroot_seeds",
+ BeetrootSoup => "minecraft:beetroot_soup",
+ DragonBreath => "minecraft:dragon_breath",
+ SplashPotion => "minecraft:splash_potion",
+ SpectralArrow => "minecraft:spectral_arrow",
+ TippedArrow => "minecraft:tipped_arrow",
+ LingeringPotion => "minecraft:lingering_potion",
+ Shield => "minecraft:shield",
+ TotemOfUndying => "minecraft:totem_of_undying",
+ ShulkerShell => "minecraft:shulker_shell",
+ IronNugget => "minecraft:iron_nugget",
+ KnowledgeBook => "minecraft:knowledge_book",
+ DebugStick => "minecraft:debug_stick",
+ MusicDisc13 => "minecraft:music_disc_13",
+ MusicDiscCat => "minecraft:music_disc_cat",
+ MusicDiscBlocks => "minecraft:music_disc_blocks",
+ MusicDiscChirp => "minecraft:music_disc_chirp",
+ MusicDiscFar => "minecraft:music_disc_far",
+ MusicDiscMall => "minecraft:music_disc_mall",
+ MusicDiscMellohi => "minecraft:music_disc_mellohi",
+ MusicDiscStal => "minecraft:music_disc_stal",
+ MusicDiscStrad => "minecraft:music_disc_strad",
+ MusicDiscWard => "minecraft:music_disc_ward",
+ MusicDisc11 => "minecraft:music_disc_11",
+ MusicDiscWait => "minecraft:music_disc_wait",
+ MusicDiscOtherside => "minecraft:music_disc_otherside",
+ MusicDisc5 => "minecraft:music_disc_5",
+ MusicDiscPigstep => "minecraft:music_disc_pigstep",
+ DiscFragment5 => "minecraft:disc_fragment_5",
+ Trident => "minecraft:trident",
+ PhantomMembrane => "minecraft:phantom_membrane",
+ NautilusShell => "minecraft:nautilus_shell",
+ HeartOfTheSea => "minecraft:heart_of_the_sea",
+ Crossbow => "minecraft:crossbow",
+ SuspiciousStew => "minecraft:suspicious_stew",
+ Loom => "minecraft:loom",
+ FlowerBannerPattern => "minecraft:flower_banner_pattern",
+ CreeperBannerPattern => "minecraft:creeper_banner_pattern",
+ SkullBannerPattern => "minecraft:skull_banner_pattern",
+ MojangBannerPattern => "minecraft:mojang_banner_pattern",
+ GlobeBannerPattern => "minecraft:globe_banner_pattern",
+ PiglinBannerPattern => "minecraft:piglin_banner_pattern",
+ GoatHorn => "minecraft:goat_horn",
+ Composter => "minecraft:composter",
+ Barrel => "minecraft:barrel",
+ Smoker => "minecraft:smoker",
+ BlastFurnace => "minecraft:blast_furnace",
+ CartographyTable => "minecraft:cartography_table",
+ FletchingTable => "minecraft:fletching_table",
+ Grindstone => "minecraft:grindstone",
+ SmithingTable => "minecraft:smithing_table",
+ Stonecutter => "minecraft:stonecutter",
+ Bell => "minecraft:bell",
+ Lantern => "minecraft:lantern",
+ SoulLantern => "minecraft:soul_lantern",
+ SweetBerries => "minecraft:sweet_berries",
+ GlowBerries => "minecraft:glow_berries",
+ Campfire => "minecraft:campfire",
+ SoulCampfire => "minecraft:soul_campfire",
+ Shroomlight => "minecraft:shroomlight",
+ Honeycomb => "minecraft:honeycomb",
+ BeeNest => "minecraft:bee_nest",
+ Beehive => "minecraft:beehive",
+ HoneyBottle => "minecraft:honey_bottle",
+ HoneycombBlock => "minecraft:honeycomb_block",
+ Lodestone => "minecraft:lodestone",
+ CryingObsidian => "minecraft:crying_obsidian",
+ Blackstone => "minecraft:blackstone",
+ BlackstoneSlab => "minecraft:blackstone_slab",
+ BlackstoneStairs => "minecraft:blackstone_stairs",
+ GildedBlackstone => "minecraft:gilded_blackstone",
+ PolishedBlackstone => "minecraft:polished_blackstone",
+ PolishedBlackstoneSlab => "minecraft:polished_blackstone_slab",
+ PolishedBlackstoneStairs => "minecraft:polished_blackstone_stairs",
+ ChiseledPolishedBlackstone => "minecraft:chiseled_polished_blackstone",
+ PolishedBlackstoneBricks => "minecraft:polished_blackstone_bricks",
+ PolishedBlackstoneBrickSlab => "minecraft:polished_blackstone_brick_slab",
+ PolishedBlackstoneBrickStairs => "minecraft:polished_blackstone_brick_stairs",
+ CrackedPolishedBlackstoneBricks => "minecraft:cracked_polished_blackstone_bricks",
+ RespawnAnchor => "minecraft:respawn_anchor",
+ Candle => "minecraft:candle",
+ WhiteCandle => "minecraft:white_candle",
+ OrangeCandle => "minecraft:orange_candle",
+ MagentaCandle => "minecraft:magenta_candle",
+ LightBlueCandle => "minecraft:light_blue_candle",
+ YellowCandle => "minecraft:yellow_candle",
+ LimeCandle => "minecraft:lime_candle",
+ PinkCandle => "minecraft:pink_candle",
+ GrayCandle => "minecraft:gray_candle",
+ LightGrayCandle => "minecraft:light_gray_candle",
+ CyanCandle => "minecraft:cyan_candle",
+ PurpleCandle => "minecraft:purple_candle",
+ BlueCandle => "minecraft:blue_candle",
+ BrownCandle => "minecraft:brown_candle",
+ GreenCandle => "minecraft:green_candle",
+ RedCandle => "minecraft:red_candle",
+ BlackCandle => "minecraft:black_candle",
+ SmallAmethystBud => "minecraft:small_amethyst_bud",
+ MediumAmethystBud => "minecraft:medium_amethyst_bud",
+ LargeAmethystBud => "minecraft:large_amethyst_bud",
+ AmethystCluster => "minecraft:amethyst_cluster",
+ PointedDripstone => "minecraft:pointed_dripstone",
+ OchreFroglight => "minecraft:ochre_froglight",
+ VerdantFroglight => "minecraft:verdant_froglight",
+ PearlescentFroglight => "minecraft:pearlescent_froglight",
+ Frogspawn => "minecraft:frogspawn",
+ EchoShard => "minecraft:echo_shard",
+ Brush => "minecraft:brush",
+ NetheriteUpgradeSmithingTemplate => "minecraft:netherite_upgrade_smithing_template",
+ SentryArmorTrimSmithingTemplate => "minecraft:sentry_armor_trim_smithing_template",
+ DuneArmorTrimSmithingTemplate => "minecraft:dune_armor_trim_smithing_template",
+ CoastArmorTrimSmithingTemplate => "minecraft:coast_armor_trim_smithing_template",
+ WildArmorTrimSmithingTemplate => "minecraft:wild_armor_trim_smithing_template",
+ WardArmorTrimSmithingTemplate => "minecraft:ward_armor_trim_smithing_template",
+ EyeArmorTrimSmithingTemplate => "minecraft:eye_armor_trim_smithing_template",
+ VexArmorTrimSmithingTemplate => "minecraft:vex_armor_trim_smithing_template",
+ TideArmorTrimSmithingTemplate => "minecraft:tide_armor_trim_smithing_template",
+ SnoutArmorTrimSmithingTemplate => "minecraft:snout_armor_trim_smithing_template",
+ RibArmorTrimSmithingTemplate => "minecraft:rib_armor_trim_smithing_template",
+ SpireArmorTrimSmithingTemplate => "minecraft:spire_armor_trim_smithing_template",
+ PotteryShardArcher => "minecraft:pottery_shard_archer",
+ PotteryShardPrize => "minecraft:pottery_shard_prize",
+ PotteryShardArmsUp => "minecraft:pottery_shard_arms_up",
+ PotteryShardSkull => "minecraft:pottery_shard_skull",
+}
+}
diff --git a/azalea-world/Cargo.toml b/azalea-world/Cargo.toml
index 113125a4..81984900 100644
--- a/azalea-world/Cargo.toml
+++ b/azalea-world/Cargo.toml
@@ -15,6 +15,7 @@ azalea-chat = { path = "../azalea-chat", version = "^0.6.0" }
azalea-core = { path = "../azalea-core", version = "^0.6.0", features = [
"bevy_ecs",
] }
+azalea-inventory = { version = "0.1.0", path = "../azalea-inventory" }
azalea-nbt = { path = "../azalea-nbt", version = "^0.6.0" }
azalea-registry = { path = "../azalea-registry", version = "^0.6.0" }
bevy_app = "0.10.0"
@@ -30,3 +31,6 @@ uuid = "1.1.2"
[profile.release]
lto = true
+
+[dev-dependencies]
+azalea-client = { path = "../azalea-client" }
diff --git a/azalea-world/README.md b/azalea-world/README.md
index 6f68ab42..9b237db0 100755
--- a/azalea-world/README.md
+++ b/azalea-world/README.md
@@ -1,3 +1 @@
-# Azalea World
-
The Minecraft world representation used in Azalea.
diff --git a/azalea-world/src/container.rs b/azalea-world/src/container.rs
index c8af8c99..2cf8da8e 100644
--- a/azalea-world/src/container.rs
+++ b/azalea-world/src/container.rs
@@ -8,7 +8,7 @@ use std::{
sync::{Arc, Weak},
};
-use crate::{ChunkStorage, Instance};
+use crate::{entity::WorldName, ChunkStorage, Instance};
/// A container of [`Instance`]s (aka worlds). Instances are stored as a Weak
/// pointer here, so if no clients are using an instance it will be forgotten.
@@ -37,7 +37,7 @@ impl InstanceContainer {
}
/// Get a world from the container.
- pub fn get(&self, name: &ResourceLocation) -> Option<Arc<RwLock<Instance>>> {
+ pub fn get(&self, name: &WorldName) -> Option<Arc<RwLock<Instance>>> {
self.worlds.get(name).and_then(|world| world.upgrade())
}
diff --git a/azalea-world/src/entity/data.rs b/azalea-world/src/entity/data.rs
index c761a786..31d4ca2f 100755
--- a/azalea-world/src/entity/data.rs
+++ b/azalea-world/src/entity/data.rs
@@ -4,7 +4,8 @@ use azalea_buf::{
BufReadError, McBuf, McBufReadable, McBufVarReadable, McBufVarWritable, McBufWritable,
};
use azalea_chat::FormattedText;
-use azalea_core::{BlockPos, Direction, GlobalPos, Particle, Slot, Vec3};
+use azalea_core::{BlockPos, Direction, GlobalPos, Particle, Vec3};
+use azalea_inventory::ItemSlot;
use bevy_ecs::component::Component;
use derive_more::Deref;
use enum_as_inner::EnumAsInner;
@@ -60,7 +61,7 @@ pub enum EntityDataValue {
String(String),
FormattedText(FormattedText),
OptionalFormattedText(Option<FormattedText>),
- ItemStack(Slot),
+ ItemStack(ItemSlot),
Boolean(bool),
Rotations(Rotations),
BlockPos(BlockPos),
diff --git a/azalea-world/src/entity/info.rs b/azalea-world/src/entity/info.rs
index fdfe82c2..525b57fa 100644
--- a/azalea-world/src/entity/info.rs
+++ b/azalea-world/src/entity/info.rs
@@ -29,7 +29,7 @@ use std::{
};
use uuid::Uuid;
-use super::Local;
+use super::{Local, LookDirection};
/// A Bevy [`SystemSet`] for various types of entity updates.
#[derive(SystemSet, Debug, Hash, Eq, PartialEq, Clone)]
@@ -75,6 +75,7 @@ impl Plugin for EntityPlugin {
debug_detect_updates_received_on_local_entities,
add_dead,
update_bounding_box,
+ clamp_look_direction,
))
.init_resource::<EntityInfos>();
}
@@ -218,10 +219,10 @@ fn update_entity_chunk_positions(
),
Changed<entity::Position>,
>,
- world_container: Res<InstanceContainer>,
+ instance_container: Res<InstanceContainer>,
) {
for (entity, pos, last_pos, world_name) in query.iter_mut() {
- let world_lock = world_container.get(world_name).unwrap();
+ let world_lock = instance_container.get(world_name).unwrap();
let mut world = world_lock.write();
let old_chunk = ChunkPos::from(*last_pos);
@@ -285,11 +286,11 @@ fn debug_detect_updates_received_on_local_entities(
fn remove_despawned_entities_from_indexes(
mut commands: Commands,
mut entity_infos: ResMut<EntityInfos>,
- world_container: Res<InstanceContainer>,
+ instance_container: Res<InstanceContainer>,
query: Query<(Entity, &EntityUuid, &Position, &WorldName, &LoadedBy), Changed<LoadedBy>>,
) {
for (entity, uuid, position, world_name, loaded_by) in &query {
- let world_lock = world_container.get(world_name).unwrap();
+ let world_lock = instance_container.get(world_name).unwrap();
let mut world = world_lock.write();
// if the entity has no references left, despawn it
@@ -322,6 +323,13 @@ fn remove_despawned_entities_from_indexes(
}
}
+pub fn clamp_look_direction(mut query: Query<&mut LookDirection>) {
+ for mut look_direction in &mut query {
+ look_direction.y_rot %= 360.0;
+ look_direction.x_rot = look_direction.x_rot.clamp(-90.0, 90.0) % 360.0;
+ }
+}
+
impl Debug for EntityInfos {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("EntityInfos").finish()
diff --git a/azalea-world/src/entity/metadata.rs b/azalea-world/src/entity/metadata.rs
index f7b19744..ed7aa40c 100644
--- a/azalea-world/src/entity/metadata.rs
+++ b/azalea-world/src/entity/metadata.rs
@@ -8,7 +8,8 @@ use super::{
SnifferState, VillagerData,
};
use azalea_chat::FormattedText;
-use azalea_core::{BlockPos, Direction, Particle, Slot, Vec3};
+use azalea_core::{BlockPos, Direction, Particle, Vec3};
+use azalea_inventory::ItemSlot;
use bevy_ecs::{bundle::Bundle, component::Component};
use derive_more::{Deref, DerefMut};
use thiserror::Error;
@@ -2140,7 +2141,7 @@ impl Default for DrownedMetadataBundle {
}
#[derive(Component, Deref, DerefMut, Clone)]
-pub struct EggItemStack(pub Slot);
+pub struct EggItemStack(pub ItemSlot);
#[derive(Component)]
pub struct Egg;
impl Egg {
@@ -2186,7 +2187,7 @@ impl Default for EggMetadataBundle {
pose: Pose::default(),
ticks_frozen: TicksFrozen(0),
},
- egg_item_stack: EggItemStack(Slot::Empty),
+ egg_item_stack: EggItemStack(ItemSlot::Empty),
}
}
}
@@ -2397,7 +2398,7 @@ impl Default for EnderDragonMetadataBundle {
}
#[derive(Component, Deref, DerefMut, Clone)]
-pub struct EnderPearlItemStack(pub Slot);
+pub struct EnderPearlItemStack(pub ItemSlot);
#[derive(Component)]
pub struct EnderPearl;
impl EnderPearl {
@@ -2443,7 +2444,7 @@ impl Default for EnderPearlMetadataBundle {
pose: Pose::default(),
ticks_frozen: TicksFrozen(0),
},
- ender_pearl_item_stack: EnderPearlItemStack(Slot::Empty),
+ ender_pearl_item_stack: EnderPearlItemStack(ItemSlot::Empty),
}
}
}
@@ -2733,7 +2734,7 @@ impl Default for EvokerFangsMetadataBundle {
}
#[derive(Component, Deref, DerefMut, Clone)]
-pub struct ExperienceBottleItemStack(pub Slot);
+pub struct ExperienceBottleItemStack(pub ItemSlot);
#[derive(Component)]
pub struct ExperienceBottle;
impl ExperienceBottle {
@@ -2779,7 +2780,7 @@ impl Default for ExperienceBottleMetadataBundle {
pose: Pose::default(),
ticks_frozen: TicksFrozen(0),
},
- experience_bottle_item_stack: ExperienceBottleItemStack(Slot::Empty),
+ experience_bottle_item_stack: ExperienceBottleItemStack(ItemSlot::Empty),
}
}
}
@@ -2830,7 +2831,7 @@ impl Default for ExperienceOrbMetadataBundle {
}
#[derive(Component, Deref, DerefMut, Clone)]
-pub struct EyeOfEnderItemStack(pub Slot);
+pub struct EyeOfEnderItemStack(pub ItemSlot);
#[derive(Component)]
pub struct EyeOfEnder;
impl EyeOfEnder {
@@ -2876,7 +2877,7 @@ impl Default for EyeOfEnderMetadataBundle {
pose: Pose::default(),
ticks_frozen: TicksFrozen(0),
},
- eye_of_ender_item_stack: EyeOfEnderItemStack(Slot::Empty),
+ eye_of_ender_item_stack: EyeOfEnderItemStack(ItemSlot::Empty),
}
}
}
@@ -2934,7 +2935,7 @@ impl Default for FallingBlockMetadataBundle {
}
#[derive(Component, Deref, DerefMut, Clone)]
-pub struct FireballItemStack(pub Slot);
+pub struct FireballItemStack(pub ItemSlot);
#[derive(Component)]
pub struct Fireball;
impl Fireball {
@@ -2980,13 +2981,13 @@ impl Default for FireballMetadataBundle {
pose: Pose::default(),
ticks_frozen: TicksFrozen(0),
},
- fireball_item_stack: FireballItemStack(Slot::Empty),
+ fireball_item_stack: FireballItemStack(ItemSlot::Empty),
}
}
}
#[derive(Component, Deref, DerefMut, Clone)]
-pub struct FireworksItem(pub Slot);
+pub struct FireworksItem(pub ItemSlot);
#[derive(Component, Deref, DerefMut, Clone)]
pub struct AttachedToTarget(pub OptionalUnsignedInt);
#[derive(Component, Deref, DerefMut, Clone)]
@@ -3044,7 +3045,7 @@ impl Default for FireworkRocketMetadataBundle {
pose: Pose::default(),
ticks_frozen: TicksFrozen(0),
},
- fireworks_item: FireworksItem(Slot::Empty),
+ fireworks_item: FireworksItem(ItemSlot::Empty),
attached_to_target: AttachedToTarget(OptionalUnsignedInt(None)),
shot_at_angle: ShotAtAngle(false),
}
@@ -3521,7 +3522,7 @@ impl Default for GiantMetadataBundle {
}
#[derive(Component, Deref, DerefMut, Clone)]
-pub struct ItemFrameItem(pub Slot);
+pub struct ItemFrameItem(pub ItemSlot);
#[derive(Component, Deref, DerefMut, Clone)]
pub struct Rotation(pub i32);
#[derive(Component)]
@@ -3567,7 +3568,7 @@ impl Default for GlowItemFrameMetadataBundle {
pose: Pose::default(),
ticks_frozen: TicksFrozen(0),
},
- item_frame_item: ItemFrameItem(Slot::Empty),
+ item_frame_item: ItemFrameItem(ItemSlot::Empty),
rotation: Rotation(0),
},
}
@@ -4356,7 +4357,7 @@ impl Default for IronGolemMetadataBundle {
}
#[derive(Component, Deref, DerefMut, Clone)]
-pub struct ItemItem(pub Slot);
+pub struct ItemItem(pub ItemSlot);
#[derive(Component)]
pub struct Item;
impl Item {
@@ -4402,7 +4403,7 @@ impl Default for ItemMetadataBundle {
pose: Pose::default(),
ticks_frozen: TicksFrozen(0),
},
- item_item: ItemItem(Slot::Empty),
+ item_item: ItemItem(ItemSlot::Empty),
}
}
}
@@ -4436,7 +4437,7 @@ pub struct ItemDisplayHeight(pub f32);
#[derive(Component, Deref, DerefMut, Clone)]
pub struct ItemDisplayGlowColorOverride(pub i32);
#[derive(Component, Deref, DerefMut, Clone)]
-pub struct ItemDisplayItemStack(pub Slot);
+pub struct ItemDisplayItemStack(pub ItemSlot);
#[derive(Component, Deref, DerefMut, Clone)]
pub struct ItemDisplayItemDisplay(pub u8);
#[derive(Component)]
@@ -4580,7 +4581,7 @@ impl Default for ItemDisplayMetadataBundle {
item_display_width: ItemDisplayWidth(0.0),
item_display_height: ItemDisplayHeight(0.0),
item_display_glow_color_override: ItemDisplayGlowColorOverride(-1),
- item_display_item_stack: ItemDisplayItemStack(Slot::Empty),
+ item_display_item_stack: ItemDisplayItemStack(ItemSlot::Empty),
item_display_item_display: ItemDisplayItemDisplay(Default::default()),
}
}
@@ -4635,7 +4636,7 @@ impl Default for ItemFrameMetadataBundle {
pose: Pose::default(),
ticks_frozen: TicksFrozen(0),
},
- item_frame_item: ItemFrameItem(Slot::Empty),
+ item_frame_item: ItemFrameItem(ItemSlot::Empty),
rotation: Rotation(0),
}
}
@@ -6192,7 +6193,7 @@ impl Default for PolarBearMetadataBundle {
}
#[derive(Component, Deref, DerefMut, Clone)]
-pub struct PotionItemStack(pub Slot);
+pub struct PotionItemStack(pub ItemSlot);
#[derive(Component)]
pub struct Potion;
impl Potion {
@@ -6238,7 +6239,7 @@ impl Default for PotionMetadataBundle {
pose: Pose::default(),
ticks_frozen: TicksFrozen(0),
},
- potion_item_stack: PotionItemStack(Slot::Empty),
+ potion_item_stack: PotionItemStack(ItemSlot::Empty),
}
}
}
@@ -7070,7 +7071,7 @@ impl Default for SlimeMetadataBundle {
}
#[derive(Component, Deref, DerefMut, Clone)]
-pub struct SmallFireballItemStack(pub Slot);
+pub struct SmallFireballItemStack(pub ItemSlot);
#[derive(Component)]
pub struct SmallFireball;
impl SmallFireball {
@@ -7116,7 +7117,7 @@ impl Default for SmallFireballMetadataBundle {
pose: Pose::default(),
ticks_frozen: TicksFrozen(0),
},
- small_fireball_item_stack: SmallFireballItemStack(Slot::Empty),
+ small_fireball_item_stack: SmallFireballItemStack(ItemSlot::Empty),
}
}
}
@@ -7281,7 +7282,7 @@ impl Default for SnowGolemMetadataBundle {
}
#[derive(Component, Deref, DerefMut, Clone)]
-pub struct SnowballItemStack(pub Slot);
+pub struct SnowballItemStack(pub ItemSlot);
#[derive(Component)]
pub struct Snowball;
impl Snowball {
@@ -7327,7 +7328,7 @@ impl Default for SnowballMetadataBundle {
pose: Pose::default(),
ticks_frozen: TicksFrozen(0),
},
- snowball_item_stack: SnowballItemStack(Slot::Empty),
+ snowball_item_stack: SnowballItemStack(ItemSlot::Empty),
}
}
}
diff --git a/azalea-world/src/entity/mod.rs b/azalea-world/src/entity/mod.rs
index 84c183ff..d1d29a5a 100644
--- a/azalea-world/src/entity/mod.rs
+++ b/azalea-world/src/entity/mod.rs
@@ -23,7 +23,8 @@ pub use data::*;
use derive_more::{Deref, DerefMut};
pub use dimensions::{update_bounding_box, EntityDimensions};
pub use info::{
- EntityInfos, EntityPlugin, EntityUpdateSet, LoadedBy, PartialEntityInfos, RelativeEntityUpdate,
+ clamp_look_direction, EntityInfos, EntityPlugin, EntityUpdateSet, LoadedBy, PartialEntityInfos,
+ RelativeEntityUpdate,
};
use std::fmt::Debug;
use uuid::Uuid;
@@ -38,19 +39,18 @@ impl std::hash::Hash for MinecraftEntityId {
}
}
impl nohash_hasher::IsEnabled for MinecraftEntityId {}
-pub fn set_rotation(physics: &mut Physics, y_rot: f32, x_rot: f32) {
- physics.y_rot = y_rot % 360.0;
- physics.x_rot = x_rot.clamp(-90.0, 90.0) % 360.0;
- // TODO: minecraft also sets yRotO and xRotO to xRot and yRot ... but
- // idk what they're used for so
-}
-pub fn move_relative(physics: &mut Physics, speed: f32, acceleration: &Vec3) {
- let input_vector = input_vector(physics, speed, acceleration);
+pub fn move_relative(
+ physics: &mut Physics,
+ direction: &LookDirection,
+ speed: f32,
+ acceleration: &Vec3,
+) {
+ let input_vector = input_vector(direction, speed, acceleration);
physics.delta += input_vector;
}
-pub fn input_vector(physics: &mut Physics, speed: f32, acceleration: &Vec3) -> Vec3 {
+pub fn input_vector(direction: &LookDirection, speed: f32, acceleration: &Vec3) -> Vec3 {
let distance = acceleration.length_squared();
if distance < 1.0E-7 {
return Vec3::default();
@@ -61,8 +61,8 @@ pub fn input_vector(physics: &mut Physics, speed: f32, acceleration: &Vec3) -> V
*acceleration
}
.scale(speed as f64);
- let y_rot = f32::sin(physics.y_rot * 0.017453292f32);
- let x_rot = f32::cos(physics.y_rot * 0.017453292f32);
+ let y_rot = f32::sin(direction.y_rot * 0.017453292f32);
+ let x_rot = f32::cos(direction.y_rot * 0.017453292f32);
Vec3 {
x: acceleration.x * (x_rot as f64) - acceleration.z * (y_rot as f64),
y: acceleration.y,
@@ -70,6 +70,20 @@ pub fn input_vector(physics: &mut Physics, speed: f32, acceleration: &Vec3) -> V
}
}
+pub fn view_vector(look_direction: &LookDirection) -> Vec3 {
+ let x_rot = look_direction.x_rot * 0.017453292;
+ let y_rot = -look_direction.y_rot * 0.017453292;
+ let y_rot_cos = f32::cos(y_rot);
+ let y_rot_sin = f32::sin(y_rot);
+ let x_rot_cos = f32::cos(x_rot);
+ let x_rot_sin = f32::sin(x_rot);
+ Vec3 {
+ x: (y_rot_sin * x_rot_cos) as f64,
+ y: (-x_rot_sin) as f64,
+ z: (y_rot_cos * x_rot_cos) as f64,
+ }
+}
+
/// Get the position of the block below the entity, but a little lower.
pub fn on_pos_legacy(chunk_storage: &ChunkStorage, position: &Position) -> BlockPos {
on_pos(0.2, chunk_storage, position)
@@ -128,6 +142,11 @@ impl Debug for EntityUuid {
/// automatically.
#[derive(Component, Clone, Copy, Debug, Default, PartialEq, Deref, DerefMut)]
pub struct Position(Vec3);
+impl From<&Position> for Vec3 {
+ fn from(value: &Position) -> Self {
+ value.0
+ }
+}
impl From<Position> for ChunkPos {
fn from(value: Position) -> Self {
ChunkPos::from(&value.0)
@@ -149,9 +168,14 @@ impl From<&Position> for BlockPos {
}
}
-/// The last position of the entity that was sent to the network.
+/// The last position of the entity that was sent over the network.
#[derive(Component, Clone, Copy, Debug, Default, PartialEq, Deref, DerefMut)]
pub struct LastSentPosition(Vec3);
+impl From<&LastSentPosition> for Vec3 {
+ fn from(value: &LastSentPosition) -> Self {
+ value.0
+ }
+}
impl From<LastSentPosition> for ChunkPos {
fn from(value: LastSentPosition) -> Self {
ChunkPos::from(&value.0)
@@ -182,9 +206,16 @@ pub struct WorldName(pub ResourceLocation);
///
/// If this is true, the entity will try to jump every tick. (It's equivalent to
/// the space key being held in vanilla.)
-#[derive(Debug, Component, Deref, DerefMut)]
+#[derive(Debug, Component, Clone, Deref, DerefMut)]
pub struct Jumping(bool);
+/// A component that contains the direction an entity is looking.
+#[derive(Debug, Component, Clone, Default)]
+pub struct LookDirection {
+ pub x_rot: f32,
+ pub y_rot: f32,
+}
+
/// The physics data relating to the entity, such as position, velocity, and
/// bounding box.
#[derive(Debug, Component)]
@@ -198,12 +229,6 @@ pub struct Physics {
/// Z acceleration.
pub zza: f32,
- pub x_rot: f32,
- pub y_rot: f32,
-
- pub x_rot_last: f32,
- pub y_rot_last: f32,
-
pub on_ground: bool,
pub last_on_ground: bool,
@@ -237,10 +262,38 @@ pub fn add_dead(mut commands: Commands, query: Query<(Entity, &Health), Changed<
}
}
+/// A component that contains the offset of the entity's eyes from the entity
+/// coordinates.
+///
+/// This is used to calculate the camera position for players, when spectating
+/// an entity, and when raytracing from the entity.
+#[derive(Component, Clone, Copy, Debug, PartialEq, Deref, DerefMut)]
+pub struct EyeHeight(f32);
+impl From<EyeHeight> for f32 {
+ fn from(value: EyeHeight) -> Self {
+ value.0
+ }
+}
+impl From<EyeHeight> for f64 {
+ fn from(value: EyeHeight) -> Self {
+ value.0 as f64
+ }
+}
+impl From<&EyeHeight> for f32 {
+ fn from(value: &EyeHeight) -> Self {
+ value.0
+ }
+}
+impl From<&EyeHeight> for f64 {
+ fn from(value: &EyeHeight) -> Self {
+ value.0 as f64
+ }
+}
+
/// A component NewType for [`azalea_registry::EntityKind`].
///
/// Most of the time, you should be using `azalea_registry::EntityKind`
-/// instead.
+/// directly instead.
#[derive(Component, Clone, Copy, Debug, PartialEq, Deref)]
pub struct EntityKind(pub azalea_registry::EntityKind);
@@ -254,6 +307,8 @@ pub struct EntityBundle {
pub position: Position,
pub last_sent_position: LastSentPosition,
pub physics: Physics,
+ pub direction: LookDirection,
+ pub eye_height: EyeHeight,
pub attributes: Attributes,
pub jumping: Jumping,
}
@@ -265,11 +320,12 @@ impl EntityBundle {
kind: azalea_registry::EntityKind,
world_name: ResourceLocation,
) -> Self {
- // TODO: get correct entity dimensions by having them codegened somewhere
+ // TODO: get correct entity dimensions by having them codegen'd somewhere
let dimensions = EntityDimensions {
width: 0.6,
height: 1.8,
};
+ let eye_height = dimensions.height * 0.85;
Self {
kind: EntityKind(kind),
@@ -284,12 +340,6 @@ impl EntityBundle {
yya: 0.,
zza: 0.,
- x_rot: 0.,
- y_rot: 0.,
-
- y_rot_last: 0.,
- x_rot_last: 0.,
-
on_ground: false,
last_on_ground: false,
@@ -299,6 +349,8 @@ impl EntityBundle {
has_impulse: false,
},
+ eye_height: EyeHeight(eye_height),
+ direction: LookDirection::default(),
attributes: Attributes {
// TODO: do the correct defaults for everything, some
diff --git a/azalea-world/src/world.rs b/azalea-world/src/world.rs
index e8d25032..a2b351c2 100644
--- a/azalea-world/src/world.rs
+++ b/azalea-world/src/world.rs
@@ -59,11 +59,11 @@ pub fn deduplicate_entities(
(Changed<MinecraftEntityId>, Without<Local>),
>,
mut loaded_by_query: Query<&mut LoadedBy>,
- world_container: Res<InstanceContainer>,
+ instance_container: Res<InstanceContainer>,
) {
// if this entity already exists, remove it
for (new_entity, id, world_name) in query.iter_mut() {
- if let Some(world_lock) = world_container.get(world_name) {
+ if let Some(world_lock) = instance_container.get(world_name) {
let world = world_lock.write();
if let Some(old_entity) = world.entity_by_id.get(id) {
if old_entity == &new_entity {
@@ -104,11 +104,11 @@ pub fn deduplicate_local_entities(
(Entity, &MinecraftEntityId, &WorldName),
(Changed<MinecraftEntityId>, With<Local>),
>,
- world_container: Res<InstanceContainer>,
+ instance_container: Res<InstanceContainer>,
) {
// if this entity already exists, remove the old one
for (new_entity, id, world_name) in query.iter_mut() {
- if let Some(world_lock) = world_container.get(world_name) {
+ if let Some(world_lock) = instance_container.get(world_name) {
let world = world_lock.write();
if let Some(old_entity) = world.entity_by_id.get(id) {
if old_entity == &new_entity {
@@ -154,11 +154,11 @@ pub fn update_uuid_index(
// mut commands: Commands,
// partial_entity_infos: &mut PartialEntityInfos,
// chunk: &ChunkPos,
-// world_container: &WorldContainer,
+// instance_container: &WorldContainer,
// world_name: &WorldName,
// mut query: Query<(&MinecraftEntityId, &mut ReferenceCount)>,
// ) {
-// let world_lock = world_container.get(world_name).unwrap();
+// let world_lock = instance_container.get(world_name).unwrap();
// let world = world_lock.read();
// if let Some(entities) = world.entities_by_chunk.get(chunk).cloned() {
@@ -195,6 +195,12 @@ impl Instance {
///
/// Note that this is sorted by `x+y+z` and not `x^2+y^2+z^2`, for
/// optimization purposes.
+ ///
+ /// ```
+ /// # fn example(client: &azalea_client::Client) {
+ /// client.world().read().find_block(client.position(), &azalea_registry::Block::Chest.into());
+ /// # }
+ /// ```
pub fn find_block(
&self,
nearest_to: impl Into<BlockPos>,
@@ -290,10 +296,10 @@ pub fn update_entity_by_id_index(
(Entity, &MinecraftEntityId, &WorldName, Option<&Local>),
Changed<MinecraftEntityId>,
>,
- world_container: Res<InstanceContainer>,
+ instance_container: Res<InstanceContainer>,
) {
for (entity, id, world_name, local) in query.iter_mut() {
- let world_lock = world_container.get(world_name).unwrap();
+ let world_lock = instance_container.get(world_name).unwrap();
let mut world = world_lock.write();
if local.is_none() {
if let Some(old_entity) = world.entity_by_id.get(id) {
diff --git a/azalea/Cargo.toml b/azalea/Cargo.toml
index 678d4a3b..5dbc7556 100644
--- a/azalea/Cargo.toml
+++ b/azalea/Cargo.toml
@@ -18,6 +18,7 @@ azalea-block = { version = "0.6.0", path = "../azalea-block" }
azalea-chat = { version = "0.6.0", path = "../azalea-chat" }
azalea-client = { version = "0.6.0", path = "../azalea-client" }
azalea-core = { version = "0.6.0", path = "../azalea-core" }
+azalea-inventory = { version = "0.1.0", path = "../azalea-inventory" }
azalea-physics = { version = "0.6.0", path = "../azalea-physics" }
azalea-protocol = { version = "0.6.0", path = "../azalea-protocol" }
azalea-registry = { version = "0.6.0", path = "../azalea-registry" }
diff --git a/azalea/examples/steal.rs b/azalea/examples/steal.rs
new file mode 100644
index 00000000..3302079a
--- /dev/null
+++ b/azalea/examples/steal.rs
@@ -0,0 +1,76 @@
+//! Steal all the diamonds from all the nearby chests.
+
+use azalea::{prelude::*, BlockPos};
+use azalea_inventory::operations::QuickMoveClick;
+use azalea_inventory::ItemSlot;
+use parking_lot::Mutex;
+use std::sync::Arc;
+
+#[tokio::main]
+async fn main() {
+ let account = Account::offline("bot");
+ // or let bot = Account::microsoft("email").await;
+
+ ClientBuilder::new()
+ .set_handler(handle)
+ .start(account, "localhost")
+ .await
+ .unwrap();
+}
+
+#[derive(Default, Clone, Component)]
+struct State {
+ pub checked_chests: Arc<Mutex<Vec<BlockPos>>>,
+}
+
+async fn handle(mut bot: Client, event: Event, state: State) -> anyhow::Result<()> {
+ match event {
+ Event::Chat(m) => {
+ if m.username() == Some(bot.profile.name.clone()) {
+ return Ok(());
+ };
+ if m.content() != "go" {
+ return Ok(());
+ }
+ {
+ state.checked_chests.lock().clear();
+ }
+
+ let chest_block = bot
+ .world()
+ .read()
+ .find_block(bot.position(), &azalea::Block::Chest.into());
+ // TODO: update this when find_blocks is implemented
+ let Some(chest_block) = chest_block else {
+ bot.chat("No chest found");
+ return Ok(());
+ };
+ // bot.goto(BlockPosGoal::from(chest_block));
+ let Some(chest) = bot.open_container(chest_block).await else {
+ println!("Couldn't open chest");
+ return Ok(());
+ };
+
+ println!("Getting contents");
+ for (index, slot) in chest
+ .contents()
+ .expect("we just opened the chest")
+ .iter()
+ .enumerate()
+ {
+ println!("Checking slot {index}: {slot:?}");
+ if let ItemSlot::Present(item) = slot {
+ if item.kind == azalea::Item::Diamond {
+ println!("clicking slot ^");
+ chest.click(QuickMoveClick::Left { slot: index as u16 });
+ }
+ }
+ }
+
+ println!("Done");
+ }
+ _ => {}
+ }
+
+ Ok(())
+}
diff --git a/azalea/examples/testbot.rs b/azalea/examples/testbot.rs
index a25b28e3..3fe9253c 100644
--- a/azalea/examples/testbot.rs
+++ b/azalea/examples/testbot.rs
@@ -4,7 +4,9 @@
use azalea::ecs::query::With;
use azalea::entity::metadata::Player;
-use azalea::entity::Position;
+use azalea::entity::{EyeHeight, Position};
+use azalea::interact::HitResultComponent;
+use azalea::inventory::ItemSlot;
use azalea::pathfinder::BlockPosGoal;
use azalea::{prelude::*, swarm::prelude::*, BlockPos, GameProfileComponent, WalkDirection};
use azalea::{Account, Client, Event};
@@ -46,7 +48,7 @@ async fn main() -> anyhow::Result<()> {
let mut accounts = Vec::new();
let mut states = Vec::new();
- for i in 0..5 {
+ for i in 0..1 {
accounts.push(Account::offline(&format!("bot{i}")));
states.push(State::default());
}
@@ -112,7 +114,7 @@ async fn handle(mut bot: Client, event: Event, _state: State) -> anyhow::Result<
bot.chat(&format!("You're at {pos:?}",));
}
"whereareyou" => {
- let pos = bot.component::<Position>();
+ let pos = bot.position();
bot.chat(&format!("I'm at {pos:?}",));
}
"goto" => {
@@ -122,10 +124,11 @@ async fn handle(mut bot: Client, event: Event, _state: State) -> anyhow::Result<
bot.goto(BlockPosGoal::from(target_pos));
}
"look" => {
- let entity_pos = bot.entity_component::<Position>(entity);
- let target_pos: BlockPos = entity_pos.into();
- println!("target_pos: {target_pos:?}");
- bot.look_at(target_pos.center());
+ let entity_pos = bot
+ .entity_component::<Position>(entity)
+ .up(bot.entity_component::<EyeHeight>(entity).into());
+ println!("entity_pos: {entity_pos:?}");
+ bot.look_at(entity_pos);
}
"jump" => {
bot.set_jumping(true);
@@ -140,18 +143,21 @@ async fn handle(mut bot: Client, event: Event, _state: State) -> anyhow::Result<
"lag" => {
std::thread::sleep(Duration::from_millis(1000));
}
+ "inventory" => {
+ println!("inventory: {:?}", bot.menu());
+ }
"findblock" => {
- let target_pos = bot.world().read().find_block(
- bot.component::<Position>(),
- &azalea_registry::Block::DiamondBlock.into(),
- );
+ let target_pos = bot
+ .world()
+ .read()
+ .find_block(bot.position(), &azalea::Block::DiamondBlock.into());
bot.chat(&format!("target_pos: {target_pos:?}",));
}
"gotoblock" => {
- let target_pos = bot.world().read().find_block(
- bot.component::<Position>(),
- &azalea_registry::Block::DiamondBlock.into(),
- );
+ let target_pos = bot
+ .world()
+ .read()
+ .find_block(bot.position(), &azalea::Block::DiamondBlock.into());
if let Some(target_pos) = target_pos {
// +1 to stand on top of the block
bot.goto(BlockPosGoal::from(target_pos.up(1)));
@@ -159,6 +165,49 @@ async fn handle(mut bot: Client, event: Event, _state: State) -> anyhow::Result<
bot.chat("no diamond block found");
}
}
+ "lever" => {
+ let target_pos = bot
+ .world()
+ .read()
+ .find_block(bot.position(), &azalea::Block::Lever.into());
+ let Some(target_pos) = target_pos else {
+ bot.chat("no lever found");
+ return Ok(())
+ };
+ bot.goto(BlockPosGoal::from(target_pos));
+ bot.look_at(target_pos.center());
+ bot.block_interact(target_pos);
+ }
+ "hitresult" => {
+ let hit_result = bot.get_component::<HitResultComponent>();
+ bot.chat(&format!("hit_result: {hit_result:?}",));
+ }
+ "chest" => {
+ let target_pos = bot
+ .world()
+ .read()
+ .find_block(bot.position(), &azalea::Block::Chest.into());
+ let Some(target_pos) = target_pos else {
+ bot.chat("no chest found");
+ return Ok(())
+ };
+ bot.look_at(target_pos.center());
+ let container = bot.open_container(target_pos).await;
+ println!("container: {:?}", container);
+ if let Some(container) = container {
+ if let Some(contents) = container.contents() {
+ for item in contents {
+ if let ItemSlot::Present(item) = item {
+ println!("item: {:?}", item);
+ }
+ }
+ } else {
+ println!("container was immediately closed");
+ }
+ } else {
+ println!("no container found");
+ }
+ }
_ => {}
}
}
@@ -196,7 +245,7 @@ async fn swarm_handle(
SwarmEvent::Chat(m) => {
println!("swarm chat message: {}", m.message().to_ansi());
if m.message().to_string() == "<py5> world" {
- for (name, world) in &swarm.world_container.read().worlds {
+ for (name, world) in &swarm.instance_container.read().worlds {
println!("world name: {name}");
if let Some(w) = world.upgrade() {
for chunk_pos in w.read().chunks.chunks.values() {
diff --git a/azalea/examples/todo/craft_dig_straight_down.rs b/azalea/examples/todo/craft_dig_straight_down.rs
index 4c980ccf..116cbcc2 100644
--- a/azalea/examples/todo/craft_dig_straight_down.rs
+++ b/azalea/examples/todo/craft_dig_straight_down.rs
@@ -38,17 +38,15 @@ async fn handle(bot: Client, event: Event, state: State) -> anyhow::Result<()> {
bot.goto(pathfinder::Goals::NearXZ(5, azalea::BlockXZ(0, 0)))
.await;
let chest = bot
- .open_container(&bot.world().find_block(azalea_registry::Block::Chest))
+ .open_container(&bot.world().find_block(azalea::Block::Chest))
.await
.unwrap();
- bot.take_amount(&chest, 5, |i| i.id == "#minecraft:planks")
+ bot.take_amount_from_container(&chest, 5, |i| i.id == "#minecraft:planks")
.await;
chest.close().await;
let crafting_table = bot
- .open_crafting_table(
- &bot.world.find_block(azalea_registry::Block::CraftingTable),
- )
+ .open_crafting_table(&bot.world.find_block(azalea::Block::CraftingTable))
.await
.unwrap();
bot.craft(&crafting_table, &bot.recipe_for("minecraft:sticks"))
diff --git a/azalea/src/bot.rs b/azalea/src/bot.rs
index a45ae28d..e5ea4c28 100644
--- a/azalea/src/bot.rs
+++ b/azalea/src/bot.rs
@@ -1,4 +1,5 @@
use crate::app::{App, CoreSchedule, IntoSystemAppConfig, Plugin, PluginGroup, PluginGroupBuilder};
+use crate::container::ContainerPlugin;
use crate::ecs::{
component::Component,
entity::Entity,
@@ -9,7 +10,8 @@ use crate::ecs::{
};
use azalea_core::Vec3;
use azalea_physics::{force_jump_listener, PhysicsSet};
-use azalea_world::entity::{metadata::Player, set_rotation, Jumping, Local, Physics, Position};
+use azalea_world::entity::{clamp_look_direction, EyeHeight, LookDirection};
+use azalea_world::entity::{metadata::Player, Jumping, Local, Position};
use std::f64::consts::PI;
use crate::pathfinder::PathfinderPlugin;
@@ -22,7 +24,9 @@ impl Plugin for BotPlugin {
.add_event::<JumpEvent>()
.add_systems((
insert_bot,
- look_at_listener.before(force_jump_listener),
+ look_at_listener
+ .before(force_jump_listener)
+ .before(clamp_look_direction),
jump_listener,
stop_jumping
.in_schedule(CoreSchedule::FixedUpdate)
@@ -99,12 +103,13 @@ pub struct LookAtEvent {
}
fn look_at_listener(
mut events: EventReader<LookAtEvent>,
- mut query: Query<(&Position, &mut Physics)>,
+ mut query: Query<(&Position, &EyeHeight, &mut LookDirection)>,
) {
for event in events.iter() {
- if let Ok((position, mut physics)) = query.get_mut(event.entity) {
- let (y_rot, x_rot) = direction_looking_at(position, &event.position);
- set_rotation(&mut physics, y_rot, x_rot);
+ if let Ok((position, eye_height, mut look_direction)) = query.get_mut(event.entity) {
+ let (y_rot, x_rot) =
+ direction_looking_at(&position.up(eye_height.into()), &event.position);
+ (look_direction.y_rot, look_direction.x_rot) = (y_rot, x_rot);
}
}
}
@@ -129,5 +134,6 @@ impl PluginGroup for DefaultBotPlugins {
PluginGroupBuilder::start::<Self>()
.add(BotPlugin)
.add(PathfinderPlugin)
+ .add(ContainerPlugin)
}
}
diff --git a/azalea/src/container.rs b/azalea/src/container.rs
new file mode 100644
index 00000000..0016caad
--- /dev/null
+++ b/azalea/src/container.rs
@@ -0,0 +1,140 @@
+use std::fmt::Formatter;
+
+use azalea_client::{
+ inventory::{CloseContainerEvent, ContainerClickEvent, InventoryComponent},
+ packet_handling::PacketEvent,
+ Client, TickBroadcast,
+};
+use azalea_core::BlockPos;
+use azalea_inventory::{operations::ClickOperation, ItemSlot, Menu};
+use azalea_protocol::packets::game::ClientboundGamePacket;
+use bevy_app::{App, Plugin};
+use bevy_ecs::{component::Component, prelude::EventReader, system::Commands};
+use std::fmt::Debug;
+
+pub struct ContainerPlugin;
+impl Plugin for ContainerPlugin {
+ fn build(&self, app: &mut App) {
+ app.add_system(handle_menu_opened_event);
+ }
+}
+
+pub trait ContainerClientExt {
+ async fn open_container(&mut self, pos: BlockPos) -> Option<ContainerHandle>;
+}
+
+impl ContainerClientExt for Client {
+ /// Open a container in the world, like a chest.
+ ///
+ /// ```
+ /// # use azalea::prelude::*;
+ /// # async fn example(mut bot: azalea::Client) {
+ /// let target_pos = bot
+ /// .world()
+ /// .read()
+ /// .find_block(bot.position(), &azalea::Block::Chest.into());
+ /// let Some(target_pos) = target_pos else {
+ /// bot.chat("no chest found");
+ /// return;
+ /// };
+ /// let container = bot.open_container(target_pos).await;
+ /// # }
+ /// ```
+ async fn open_container(&mut self, pos: BlockPos) -> Option<ContainerHandle> {
+ self.ecs
+ .lock()
+ .entity_mut(self.entity)
+ .insert(WaitingForInventoryOpen);
+ self.block_interact(pos);
+
+ let mut receiver = {
+ let ecs = self.ecs.lock();
+ let tick_broadcast = ecs.resource::<TickBroadcast>();
+ tick_broadcast.subscribe()
+ };
+ while receiver.recv().await.is_ok() {
+ let ecs = self.ecs.lock();
+ if ecs.get::<WaitingForInventoryOpen>(self.entity).is_none() {
+ break;
+ }
+ }
+
+ let ecs = self.ecs.lock();
+ let inventory = ecs
+ .get::<InventoryComponent>(self.entity)
+ .expect("no inventory");
+ if inventory.id == 0 {
+ None
+ } else {
+ Some(ContainerHandle {
+ id: inventory.id,
+ client: self.clone(),
+ })
+ }
+ }
+}
+
+/// A handle to the open container. The container will be closed once this is
+/// dropped.
+pub struct ContainerHandle {
+ pub id: u8,
+ client: Client,
+}
+impl Drop for ContainerHandle {
+ fn drop(&mut self) {
+ self.client.ecs.lock().send_event(CloseContainerEvent {
+ entity: self.client.entity,
+ id: self.id,
+ });
+ }
+}
+impl Debug for ContainerHandle {
+ fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+ f.debug_struct("ContainerHandle")
+ .field("id", &self.id)
+ .finish()
+ }
+}
+impl ContainerHandle {
+ /// Returns the menu of the container. If the container is closed, this
+ /// will return `None`.
+ pub fn menu(&self) -> Option<Menu> {
+ let ecs = self.client.ecs.lock();
+ let inventory = ecs
+ .get::<InventoryComponent>(self.client.entity)
+ .expect("no inventory");
+ if inventory.id == self.id {
+ Some(inventory.container_menu.clone().unwrap())
+ } else {
+ None
+ }
+ }
+
+ /// Returns the item slots in the container, not including the player's
+ /// inventory. If the container is closed, this will return `None`.
+ pub fn contents(&self) -> Option<Vec<ItemSlot>> {
+ self.menu().map(|menu| menu.contents())
+ }
+
+ pub fn click(&self, operation: impl Into<ClickOperation>) {
+ let operation = operation.into();
+ self.client.ecs.lock().send_event(ContainerClickEvent {
+ entity: self.client.entity,
+ window_id: self.id,
+ operation,
+ });
+ }
+}
+
+#[derive(Component, Debug)]
+pub struct WaitingForInventoryOpen;
+
+fn handle_menu_opened_event(mut commands: Commands, mut events: EventReader<PacketEvent>) {
+ for event in events.iter() {
+ if let ClientboundGamePacket::ContainerSetContent { .. } = event.packet {
+ commands
+ .entity(event.entity)
+ .remove::<WaitingForInventoryOpen>();
+ }
+ }
+}
diff --git a/azalea/src/lib.rs b/azalea/src/lib.rs
index bd1d356a..2e8e4fa1 100644
--- a/azalea/src/lib.rs
+++ b/azalea/src/lib.rs
@@ -1,7 +1,10 @@
#![doc = include_str!("../README.md")]
#![feature(async_closure)]
+#![allow(incomplete_features)]
+#![feature(async_fn_in_trait)]
mod bot;
+mod container;
pub mod pathfinder;
pub mod prelude;
pub mod swarm;
@@ -12,7 +15,7 @@ pub use azalea_block as blocks;
pub use azalea_client::*;
pub use azalea_core::{BlockPos, Vec3};
pub use azalea_protocol as protocol;
-pub use azalea_registry::EntityKind;
+pub use azalea_registry::{Block, EntityKind, Item};
pub use azalea_world::{entity, Instance};
use bot::DefaultBotPlugins;
use ecs::component::Component;
diff --git a/azalea/src/pathfinder/mod.rs b/azalea/src/pathfinder/mod.rs
index 9c06ebb8..56c8e0ce 100644
--- a/azalea/src/pathfinder/mod.rs
+++ b/azalea/src/pathfinder/mod.rs
@@ -93,7 +93,7 @@ fn goto_listener(
mut commands: Commands,
mut events: EventReader<GotoEvent>,
mut query: Query<(&Position, &WorldName)>,
- world_container: Res<InstanceContainer>,
+ instance_container: Res<InstanceContainer>,
) {
let thread_pool = AsyncComputeTaskPool::get();
@@ -106,7 +106,7 @@ fn goto_listener(
vertical_vel: VerticalVel::None,
};
- let world_lock = world_container
+ let world_lock = instance_container
.get(world_name)
.expect("Entity tried to pathfind but the entity isn't in a valid world");
let end = event.goal.goal_node();
diff --git a/azalea/src/prelude.rs b/azalea/src/prelude.rs
index b1a1fed3..87cb0b53 100644
--- a/azalea/src/prelude.rs
+++ b/azalea/src/prelude.rs
@@ -1,7 +1,10 @@
//! The Azalea prelude. Things that are necessary for a bare-bones bot are
//! re-exported here.
-pub use crate::{bot::BotClientExt, pathfinder::PathfinderClientExt, ClientBuilder};
+pub use crate::{
+ bot::BotClientExt, container::ContainerClientExt, pathfinder::PathfinderClientExt,
+ ClientBuilder,
+};
pub use azalea_client::{Account, Client, Event};
// this is necessary to make the macros that reference bevy_ecs work
pub use crate::ecs as bevy_ecs;
diff --git a/azalea/src/swarm/mod.rs b/azalea/src/swarm/mod.rs
index 6fe11b7d..2253f5bd 100644
--- a/azalea/src/swarm/mod.rs
+++ b/azalea/src/swarm/mod.rs
@@ -37,7 +37,7 @@ pub struct Swarm {
// bot_datas: Arc<Mutex<Vec<(Client, S)>>>,
resolved_address: SocketAddr,
address: ServerAddress,
- pub world_container: Arc<RwLock<InstanceContainer>>,
+ pub instance_container: Arc<RwLock<InstanceContainer>>,
bots_tx: mpsc::UnboundedSender<(Option<Event>, Client)>,
swarm_tx: mpsc::UnboundedSender<SwarmEvent>,
@@ -248,7 +248,7 @@ where
// resolve the address
let resolved_address = resolver::resolve_address(&address).await?;
- let world_container = Arc::new(RwLock::new(InstanceContainer::default()));
+ let instance_container = Arc::new(RwLock::new(InstanceContainer::default()));
// we can't modify the swarm plugins after this
let (bots_tx, mut bots_rx) = mpsc::unbounded_channel();
@@ -263,7 +263,7 @@ where
resolved_address,
address,
- world_container,
+ instance_container,
bots_tx,
diff --git a/codegen/genregistries.py b/codegen/genregistries.py
index 43591d59..e24dcc6a 100755
--- a/codegen/genregistries.py
+++ b/codegen/genregistries.py
@@ -1,3 +1,4 @@
+import lib.code.inventory
import lib.code.registry
import lib.code.version
import lib.code.packet
@@ -10,6 +11,7 @@ version_id = lib.code.version.get_version_id()
registries = lib.extract.get_registries_report(version_id)
lib.code.registry.generate_registries(registries)
+lib.code.inventory.update_menus(registries['minecraft:menu']['entries'])
lib.code.utils.fmt()
diff --git a/codegen/lib/code/entity.py b/codegen/lib/code/entity.py
index c787ffd4..67e818b7 100644
--- a/codegen/lib/code/entity.py
+++ b/codegen/lib/code/entity.py
@@ -12,7 +12,7 @@ DATA_RS_DIR = get_dir_location(
'../azalea-world/src/entity/data.rs')
def generate_metadata_names(burger_dataserializers: dict, mappings: Mappings):
- serializer_names = [None] * len(burger_dataserializers)
+ serializer_names: list[Optional[str]] = [None] * len(burger_dataserializers)
for burger_serializer in burger_dataserializers.values():
print(burger_serializer)
@@ -105,7 +105,8 @@ use super::{
SnifferState, VillagerData
};
use azalea_chat::FormattedText;
-use azalea_core::{BlockPos, Direction, Particle, Slot, Vec3};
+use azalea_core::{BlockPos, Direction, Particle, Vec3};
+use azalea_inventory::ItemSlot;
use bevy_ecs::{bundle::Bundle, component::Component};
use derive_more::{Deref, DerefMut};
use thiserror::Error;
@@ -425,7 +426,7 @@ impl From<EntityDataValue> for UpdateMetadataError {
elif type_name == 'OptionalUnsignedInt':
default = f'OptionalUnsignedInt(Some({default}))' if default != 'Empty' else 'OptionalUnsignedInt(None)'
elif type_name == 'ItemStack':
- default = f'Slot::Present({default})' if default != 'Empty' else 'Slot::Empty'
+ default = f'ItemSlot::Present({default})' if default != 'Empty' else 'ItemSlot::Empty'
elif type_name == 'BlockState':
default = f'{default}' if default != 'Empty' else 'azalea_block::BlockState::AIR'
elif type_name == 'OptionalBlockState':
diff --git a/codegen/lib/code/inventory.py b/codegen/lib/code/inventory.py
new file mode 100644
index 00000000..caab57f2
--- /dev/null
+++ b/codegen/lib/code/inventory.py
@@ -0,0 +1,108 @@
+from lib.utils import padded_hex, to_snake_case, to_camel_case, get_dir_location
+from lib.code.utils import burger_type_to_rust_type, write_packet_file
+from lib.mappings import Mappings
+from typing import Any, Optional
+import os
+import re
+
+# The directory where declare_menus! {} is done
+inventory_menus_dir = get_dir_location(f'../azalea-inventory/src/lib.rs')
+
+
+def update_menus(initial_menu_entries: dict[str, Any]):
+ # new_menus is a dict of { menu_id: { "protocol_id": protocol_id } }
+ # so convert that into an array where the protocol id is the index and the
+ # values are enum variant names
+ new_menus: list[str] = [''] * len(initial_menu_entries)
+ for menu_id, menu in initial_menu_entries.items():
+ new_menus[menu['protocol_id']] = menu_name_to_enum_name(menu_id)
+
+ new_menus.insert(0, 'Player')
+
+ with open(inventory_menus_dir, 'r') as f:
+ menus_rs = f.read().splitlines()
+
+ start_line_index = 0
+
+ current_menus = []
+ in_the_macro = False
+ for i, line in enumerate(menus_rs):
+ if line.startswith('declare_menus!'):
+ in_the_macro = True
+ start_line_index = i
+ if in_the_macro:
+ if line.startswith(' ') and line.endswith('{'):
+ # get the variant name for this menu
+ current_menu = line[:-1].strip()
+ current_menus.append(current_menu)
+
+ print('current_menus', current_menus)
+ print('new_menus', new_menus)
+
+ # now we have the current menus, so compare that with the expected
+ # menus and update the file if needed
+ if current_menus != new_menus:
+ # ok so insert the new menus with todo!() for the body
+ current_menus_list_index = 0
+ new_menus_list_index = 0
+ insert_line_index = start_line_index + 1
+ # figure out what menus need to be placed
+ while True:
+ # if the values at the indexes are the same, add to both and don't do anything
+ if (
+ current_menus_list_index < len(current_menus)
+ and new_menus_list_index < len(new_menus)
+ and current_menus[current_menus_list_index] == new_menus[new_menus_list_index]
+ ):
+ current_menus_list_index += 1
+ new_menus_list_index += 1
+ # increase insert_line_index until we get a line that starts with }
+ while not menus_rs[insert_line_index].strip().startswith('}'):
+ insert_line_index += 1
+ insert_line_index += 1
+ # print('same', current_menus_list_index,
+ # new_menus_list_index, insert_line_index)
+ # something was added to new_menus but not current_menus
+ elif new_menus_list_index < len(new_menus) and new_menus[new_menus_list_index] not in current_menus:
+ # insert the new menu
+ menus_rs.insert(
+ insert_line_index, f' {new_menus[new_menus_list_index]} {{\n todo!()\n }},')
+ insert_line_index += 1
+ new_menus_list_index += 1
+ print('added', current_menus_list_index,
+ new_menus_list_index, insert_line_index)
+ # something was removed from new_menus but is still in current_menus
+ elif current_menus_list_index < len(current_menus) and current_menus[current_menus_list_index] not in new_menus:
+ # remove the current menu
+ while not menus_rs[insert_line_index].strip().startswith('}'):
+ menus_rs.pop(insert_line_index)
+ menus_rs.pop(insert_line_index)
+ current_menus_list_index += 1
+ print('removed', current_menus_list_index,
+ new_menus_list_index, insert_line_index)
+
+ # if current_menus_list_index overflowed, then add the rest of the new menus
+ elif current_menus_list_index >= len(current_menus):
+ for i in range(new_menus_list_index, len(new_menus)):
+ menus_rs.insert(
+ insert_line_index, f' {new_menus[i]} {{\n todo!()\n }},')
+ insert_line_index += 1
+ print('current_menus_list_index overflowed', current_menus_list_index,
+ new_menus_list_index, insert_line_index)
+ break
+ # if new_menus_list_index overflowed, then remove the rest of the current menus
+ elif new_menus_list_index >= len(new_menus):
+ for _ in range(current_menus_list_index, len(current_menus)):
+ while not menus_rs[insert_line_index].strip().startswith('}'):
+ menus_rs.pop(insert_line_index)
+ menus_rs.pop(insert_line_index)
+ # current_menus_list_index += 1
+ print('new_menus_list_index overflowed', current_menus_list_index,
+ new_menus_list_index, insert_line_index)
+ break
+ with open(inventory_menus_dir, 'w') as f:
+ f.write('\n'.join(menus_rs))
+
+
+def menu_name_to_enum_name(menu_name: str) -> str:
+ return to_camel_case(menu_name.split(':')[-1])
diff --git a/codegen/lib/code/registry.py b/codegen/lib/code/registry.py
index a67b5e4d..e203c11a 100755
--- a/codegen/lib/code/registry.py
+++ b/codegen/lib/code/registry.py
@@ -16,12 +16,16 @@ def generate_registries(registries: dict):
# Stone => "minecraft:stone"
# });
+ registry_name = registry_name.split(':')[1]
+
if registry_name.endswith('_type'):
# change _type to _kind because that's Rustier (and because _type
# is a reserved keyword)
registry_name = registry_name[:-5] + '_kind'
+ elif registry_name in {'menu'}:
+ registry_name += '_kind'
- registry_struct_name = to_camel_case(registry_name.split(':')[1])
+ registry_struct_name = to_camel_case(registry_name)
registry_code = []
registry_code.append(f'enum {registry_struct_name} {{')
diff --git a/codegen/migrate.py b/codegen/migrate.py
index fa238561..0222ab00 100755
--- a/codegen/migrate.py
+++ b/codegen/migrate.py
@@ -1,5 +1,6 @@
from lib.code.packet import fix_state
from lib.utils import PacketIdentifier, group_packets
+import lib.code.inventory
import lib.code.language
import lib.code.registry
import lib.code.version
@@ -134,6 +135,7 @@ lib.code.language.write_language(language)
print('Generating registries...')
registries = lib.extract.get_registries_report(new_version_id)
lib.code.registry.generate_registries(registries)
+lib.code.inventory.update_menus(registries['minecraft:menu']['entries'])
print('Generating entity metadata...')
burger_entities_data = new_burger_data[0]['entities']