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