mirror of
https://github.com/mat-1/azalea.git
synced 2024-11-03 08:04:00 +00:00
Inventory (#48)
* start adding azalea-inventory * design more of how inventories are defined * start working on az-inv-macros * inventory macro works * start adding inventory codegen * update some deps * add inventory codegen * manually write inventory menus * put the inventories in Client * start on containersetcontent * inventory menu should hopefully work * checks in containersetcontent * format a comment * move some variant matches * inventory.rs * inventory stuff * more inventory stuff * inventory/container tracking works * start adding interact function * sequence number * start adding HitResultComponent * implement traverse_blocks * start adding clip * add clip function * update_hit_result_component * start trying to fix * fix * make some stuff simpler * clippy * lever * chest * container handle * fix ambiguity * fix some doc tests * move some container stuff from az-client to azalea * clicking container * start implementing simulate_click * keep working on simulate click * implement more of simulate_click this is really boring * inventory fixes * start implementing shift clicking * fix panic in azalea-chat i hope * shift clicking implemented * more inventory stuff * fix items not showing in containers sometimes * fix test * fix all warnings * remove a println --------- Co-authored-by: mat <git@matdoes.dev>
This commit is contained in:
parent
1fb4418f2c
commit
634cb8d72c
78 changed files with 5635 additions and 404 deletions
214
Cargo.lock
generated
214
Cargo.lock
generated
|
@ -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",
|
||||
]
|
||||
|
@ -324,6 +330,25 @@ dependencies = [
|
|||
"uuid",
|
||||
]
|
||||
|
||||
[[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"
|
||||
|
@ -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"
|
||||
|
@ -1601,6 +1630,15 @@ dependencies = [
|
|||
"adler",
|
||||
]
|
||||
|
||||
[[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"
|
||||
|
@ -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 = "reqwest"
|
||||
version = "0.11.16"
|
||||
name = "regex-syntax"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "27b71749df584b7f4cac2c426c127a7c785a5106cc98f7a8feb044115f0fa254"
|
||||
checksum = "a5996294f19bd3aae0453a862ad728f60e6600695733dd5df01da90c54363a3c"
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.11.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
|
@ -2818,36 +2886,72 @@ version = "0.42.2"
|
|||
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"
|
||||
|
@ -2855,10 +2959,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.4.1"
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.48.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae8970b36c66498d8ff1d66685dc86b91b29db0c7739899012f63a63814b4b28"
|
||||
checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5617da7e1f97bf363947d767b91aaf3c2bbc19db7fda9c65af1278713d58e0a2"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
|
|
@ -15,6 +15,7 @@ members = [
|
|||
"azalea-buf",
|
||||
"azalea-physics",
|
||||
"azalea-registry",
|
||||
"azalea-inventory",
|
||||
]
|
||||
|
||||
[profile.release]
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
200
azalea-client/src/interact.rs
Normal file
200
azalea-client/src/interact.rs
Normal file
|
@ -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,
|
||||
},
|
||||
)
|
||||
}
|
721
azalea-client/src/inventory.rs
Normal file
721
azalea-client/src/inventory.rs
Normal file
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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};
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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(_) => {}
|
||||
|
|
|
@ -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
|
||||
|
|
4
azalea-core/Cargo.toml
Normal file → Executable file
4
azalea-core/Cargo.toml
Normal file → Executable file
|
@ -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"
|
||||
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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::*;
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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
|
||||
|
|
12
azalea-inventory/Cargo.toml
Normal file
12
azalea-inventory/Cargo.toml
Normal file
|
@ -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" }
|
2
azalea-inventory/README.md
Normal file
2
azalea-inventory/README.md
Normal file
|
@ -0,0 +1,2 @@
|
|||
Representations of various inventory data structures in Minecraft.
|
||||
|
14
azalea-inventory/azalea-inventory-macros/Cargo.toml
Normal file
14
azalea-inventory/azalea-inventory-macros/Cargo.toml
Normal file
|
@ -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"
|
45
azalea-inventory/azalea-inventory-macros/src/lib.rs
Normal file
45
azalea-inventory/azalea-inventory-macros/src/lib.rs
Normal file
|
@ -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()
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
70
azalea-inventory/azalea-inventory-macros/src/menu_enum.rs
Normal file
70
azalea-inventory/azalea-inventory-macros/src/menu_enum.rs
Normal file
|
@ -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
|
||||
}
|
448
azalea-inventory/azalea-inventory-macros/src/menu_impl.rs
Normal file
448
azalea-inventory/azalea-inventory-macros/src/menu_impl.rs
Normal file
|
@ -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,
|
||||
"e! {
|
||||
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,
|
||||
"e! {
|
||||
#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,
|
||||
"e! {
|
||||
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,
|
||||
"e! {
|
||||
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,
|
||||
"e! {
|
||||
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
|
||||
},
|
||||
}
|
||||
}
|
69
azalea-inventory/azalea-inventory-macros/src/parse_macro.rs
Normal file
69
azalea-inventory/azalea-inventory-macros/src/parse_macro.rs
Normal file
|
@ -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 })
|
||||
}
|
||||
}
|
54
azalea-inventory/azalea-inventory-macros/src/utils.rs
Normal file
54
azalea-inventory/azalea-inventory-macros/src/utils.rs
Normal file
|
@ -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");
|
||||
}
|
||||
}
|
21
azalea-inventory/src/item/mod.rs
Normal file
21
azalea-inventory/src/item/mod.rs
Normal file
|
@ -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
|
||||
}
|
||||
}
|
172
azalea-inventory/src/lib.rs
Normal file
172
azalea-inventory/src/lib.rs
Normal file
|
@ -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,
|
||||
},
|
||||
}
|
698
azalea-inventory/src/operations.rs
Normal file
698
azalea-inventory/src/operations.rs
Normal file
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
146
azalea-inventory/src/slot.rs
Normal file
146
azalea-inventory/src/slot.rs
Normal file
|
@ -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(())
|
||||
}
|
||||
}
|
|
@ -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(|| {
|
||||
|
|
|
@ -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"
|
||||
|
|
232
azalea-physics/src/clip.rs
Normal file
232
azalea-physics/src/clip.rs
Normal file
|
@ -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, ¤t_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, ¤t_block) {
|
||||
return data;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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.,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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",
|
||||
] }
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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)?;
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -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" }
|
||||
|
|
|
@ -1,3 +1 @@
|
|||
# Azalea World
|
||||
|
||||
The Minecraft world representation used in Azalea.
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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" }
|
||||
|
|
76
azalea/examples/steal.rs
Normal file
76
azalea/examples/steal.rs
Normal file
|
@ -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(())
|
||||
}
|
|
@ -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() {
|
||||
|
|
|
@ -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"))
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
140
azalea/src/container.rs
Normal file
140
azalea/src/container.rs
Normal file
|
@ -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>();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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':
|
||||
|
|
108
codegen/lib/code/inventory.py
Normal file
108
codegen/lib/code/inventory.py
Normal file
|
@ -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])
|
|
@ -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} {{')
|
||||
|
|
|
@ -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']
|
||||
|
|
Loading…
Reference in a new issue