Inventory (#48)

* start adding azalea-inventory

* design more of how inventories are defined

* start working on az-inv-macros

* inventory macro works

* start adding inventory codegen

* update some deps

* add inventory codegen

* manually write inventory menus

* put the inventories in Client

* start on containersetcontent

* inventory menu should hopefully work

* checks in containersetcontent

* format a comment

* move some variant matches

* inventory.rs

* inventory stuff

* more inventory stuff

* inventory/container tracking works

* start adding interact function

* sequence number

* start adding HitResultComponent

* implement traverse_blocks

* start adding clip

* add clip function

* update_hit_result_component

* start trying to fix

* fix

* make some stuff simpler

* clippy

* lever

* chest

* container handle

* fix ambiguity

* fix some doc tests

* move some container stuff from az-client to azalea

* clicking container

* start implementing simulate_click

* keep working on simulate click

* implement more of simulate_click

this is really boring

* inventory fixes

* start implementing shift clicking

* fix panic in azalea-chat i hope

* shift clicking implemented

* more inventory stuff

* fix items not showing in containers sometimes

* fix test

* fix all warnings

* remove a println

---------

Co-authored-by: mat <git@matdoes.dev>
This commit is contained in:
mat 2023-05-03 20:57:27 -05:00 committed by GitHub
parent 1fb4418f2c
commit 634cb8d72c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
78 changed files with 5635 additions and 404 deletions

214
Cargo.lock generated
View file

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

View file

@ -15,6 +15,7 @@ members = [
"azalea-buf",
"azalea-physics",
"azalea-registry",
"azalea-inventory",
]
[profile.release]

View file

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

View file

@ -2,11 +2,13 @@ use crate::{
chat::ChatPlugin,
disconnect::{DisconnectEvent, DisconnectPlugin},
events::{Event, EventPlugin, LocalPlayerEvents},
interact::{CurrentSequenceNumber, InteractPlugin},
inventory::{InventoryComponent, InventoryPlugin},
local_player::{
death_event, handle_send_packet_event, update_in_loaded_chunk, GameProfileComponent,
LocalPlayer, PhysicsState, SendPacketEvent,
},
movement::PlayerMovePlugin,
movement::{LastSentLookDirection, PlayerMovePlugin},
packet_handling::{self, PacketHandlerPlugin, PacketReceiver},
player::retroactively_add_game_profile_component,
task_pool::TaskPoolPlugin,
@ -15,11 +17,13 @@ use crate::{
use azalea_auth::{game_profile::GameProfile, sessionserver::ClientSessionServerError};
use azalea_chat::FormattedText;
use azalea_core::Vec3;
use azalea_physics::{PhysicsPlugin, PhysicsSet};
use azalea_protocol::{
connect::{Connection, ConnectionError},
packets::{
game::{
clientbound_player_abilities_packet::ClientboundPlayerAbilitiesPacket,
serverbound_client_information_packet::ServerboundClientInformationPacket,
ClientboundGamePacket, ServerboundGamePacket,
},
@ -37,16 +41,17 @@ use azalea_protocol::{
resolver, ServerAddress,
};
use azalea_world::{
entity::{EntityPlugin, EntityUpdateSet, Local, WorldName},
entity::{EntityPlugin, EntityUpdateSet, Local, Position, WorldName},
Instance, InstanceContainer, PartialInstance,
};
use bevy_app::{App, CoreSchedule, Plugin, PluginGroup, PluginGroupBuilder};
use bevy_app::{App, CoreSchedule, IntoSystemAppConfig, Plugin, PluginGroup, PluginGroupBuilder};
use bevy_ecs::{
bundle::Bundle,
component::Component,
entity::Entity,
schedule::IntoSystemConfig,
schedule::{LogLevel, ScheduleBuildSettings, ScheduleLabel},
system::{ResMut, Resource},
world::World,
};
use bevy_log::LogPlugin;
@ -56,7 +61,10 @@ use log::{debug, error};
use parking_lot::{Mutex, RwLock};
use std::{collections::HashMap, fmt::Debug, io, net::SocketAddr, sync::Arc, time::Duration};
use thiserror::Error;
use tokio::{sync::mpsc, time};
use tokio::{
sync::{broadcast, mpsc},
time,
};
use uuid::Uuid;
/// `Client` has the things that a user interacting with the library will want.
@ -93,11 +101,50 @@ pub struct Client {
}
/// A component that contains some of the "settings" for this client that are
/// sent to the server, such as render distance.
/// sent to the server, such as render distance. This is only present on local
/// players.
pub type ClientInformation = ServerboundClientInformationPacket;
/// A component that contains the abilities the player has, like flying
/// or instantly breaking blocks. This is only present on local players.
#[derive(Clone, Debug, Component, Default)]
pub struct PlayerAbilities {
pub invulnerable: bool,
pub flying: bool,
pub can_fly: bool,
/// Whether the player can instantly break blocks and can duplicate blocks
/// in their inventory.
pub instant_break: bool,
pub flying_speed: f32,
/// Used for the fov
pub walking_speed: f32,
}
impl From<ClientboundPlayerAbilitiesPacket> for PlayerAbilities {
fn from(packet: ClientboundPlayerAbilitiesPacket) -> Self {
Self {
invulnerable: packet.flags.invulnerable,
flying: packet.flags.flying,
can_fly: packet.flags.can_fly,
instant_break: packet.flags.instant_break,
flying_speed: packet.flying_speed,
walking_speed: packet.walking_speed,
}
}
}
/// A component that contains a map of player UUIDs to their information in the
/// tab list
/// tab list.
///
/// ```
/// # use azalea_client::TabList;
/// # fn example(client: &azalea_client::Client) {
/// let tab_list = client.component::<TabList>();
/// println!("Online players:");
/// for (uuid, player_info) in tab_list.iter() {
/// println!("- {} ({}ms)", player_info.profile.name, player_info.latency);
/// }
/// # }
#[derive(Component, Clone, Debug, Deref, DerefMut, Default)]
pub struct TabList(HashMap<Uuid, PlayerInfo>);
@ -246,8 +293,12 @@ impl Client {
game_profile: GameProfileComponent(game_profile),
physics_state: PhysicsState::default(),
local_player_events: LocalPlayerEvents(tx),
inventory: InventoryComponent::default(),
client_information: ClientInformation::default(),
tab_list: TabList::default(),
current_sequence_number: CurrentSequenceNumber::default(),
last_sent_direction: LastSentLookDirection::default(),
abilities: PlayerAbilities::default(),
_local: Local,
});
@ -421,6 +472,11 @@ impl Client {
self.query::<&T>(&mut self.ecs.lock()).clone()
}
/// Get a component from this client, or `None` if it doesn't exist.
pub fn get_component<T: Component + Clone>(&self) -> Option<T> {
self.query::<Option<&T>>(&mut self.ecs.lock()).cloned()
}
/// Get a reference to our (potentially shared) world.
///
/// This gets the [`Instance`] from our world container. If it's a normal
@ -430,8 +486,8 @@ impl Client {
pub fn world(&self) -> Arc<RwLock<Instance>> {
let world_name = self.component::<WorldName>();
let ecs = self.ecs.lock();
let world_container = ecs.resource::<InstanceContainer>();
world_container.get(&world_name).unwrap()
let instance_container = ecs.resource::<InstanceContainer>();
instance_container.get(&world_name).unwrap()
}
/// Returns whether we have a received the login packet yet.
@ -478,6 +534,15 @@ impl Client {
}
}
impl Client {
/// Get the position of this client.
///
/// This is a shortcut for `Vec3::from(&bot.component::<Position>())`.
pub fn position(&self) -> Vec3 {
Vec3::from(&self.component::<Position>())
}
}
/// A bundle for the components that are present on a local player that received
/// a login packet. If you want to filter for this, just use [`Local`].
#[derive(Bundle)]
@ -487,8 +552,12 @@ pub struct JoinedClientBundle {
pub game_profile: GameProfileComponent,
pub physics_state: PhysicsState,
pub local_player_events: LocalPlayerEvents,
pub inventory: InventoryComponent,
pub client_information: ClientInformation,
pub tab_list: TabList,
pub current_sequence_number: CurrentSequenceNumber,
pub last_sent_direction: LastSentLookDirection,
pub abilities: PlayerAbilities,
pub _local: Local,
}
@ -498,11 +567,7 @@ impl Plugin for AzaleaPlugin {
// Minecraft ticks happen every 50ms
app.insert_resource(FixedTime::new(Duration::from_millis(50)));
app.add_system(
update_in_loaded_chunk
.after(PhysicsSet)
.after(handle_send_packet_event),
);
app.add_system(update_in_loaded_chunk.after(PhysicsSet));
// fire the Death event when the player dies.
app.add_system(death_event);
@ -599,6 +664,39 @@ pub async fn tick_run_schedule_loop(run_schedule_sender: mpsc::UnboundedSender<(
}
}
/// A resource that contains a [`broadcast::Sender`] that will be sent every
/// Minecraft tick.
///
/// This is useful for running code every schedule from async user code.
///
/// ```
/// use azalea_client::TickBroadcast;
/// # async fn example(client: azalea_client::Client) {
/// let mut receiver = {
/// let ecs = client.ecs.lock();
/// let tick_broadcast = ecs.resource::<TickBroadcast>();
/// tick_broadcast.subscribe()
/// };
/// while receiver.recv().await.is_ok() {
/// // do something
/// }
/// # }
/// ```
#[derive(Resource, Deref)]
pub struct TickBroadcast(broadcast::Sender<()>);
fn send_tick_broadcast(tick_broadcast: ResMut<TickBroadcast>) {
let _ = tick_broadcast.0.send(());
}
/// A plugin that makes the [`RanScheduleBroadcast`] resource available.
pub struct TickBroadcastPlugin;
impl Plugin for TickBroadcastPlugin {
fn build(&self, app: &mut App) {
app.insert_resource(TickBroadcast(broadcast::channel(1).0))
.add_system(send_tick_broadcast.in_schedule(CoreSchedule::FixedUpdate));
}
}
/// This plugin group will add all the default plugins necessary for Azalea to
/// work.
pub struct DefaultPlugins;
@ -614,8 +712,11 @@ impl PluginGroup for DefaultPlugins {
.add(PhysicsPlugin)
.add(EventPlugin)
.add(TaskPoolPlugin::default())
.add(InventoryPlugin)
.add(ChatPlugin)
.add(DisconnectPlugin)
.add(PlayerMovePlugin)
.add(InteractPlugin)
.add(TickBroadcastPlugin)
}
}

View file

@ -0,0 +1,200 @@
use azalea_core::{BlockHitResult, BlockPos, Direction, GameMode, Vec3};
use azalea_physics::clip::{BlockShapeType, ClipContext, FluidPickType};
use azalea_protocol::packets::game::{
serverbound_interact_packet::InteractionHand,
serverbound_use_item_on_packet::{BlockHit, ServerboundUseItemOnPacket},
};
use azalea_world::{
entity::{clamp_look_direction, view_vector, EyeHeight, LookDirection, Position, WorldName},
InstanceContainer,
};
use bevy_app::{App, Plugin};
use bevy_ecs::{
component::Component,
entity::Entity,
event::EventReader,
schedule::{IntoSystemConfig, IntoSystemConfigs},
system::{Commands, Query, Res},
};
use derive_more::{Deref, DerefMut};
use log::warn;
use crate::{
local_player::{handle_send_packet_event, LocalGameMode},
Client, LocalPlayer,
};
/// A plugin that allows clients to interact with blocks in the world.
pub struct InteractPlugin;
impl Plugin for InteractPlugin {
fn build(&self, app: &mut App) {
app.add_event::<BlockInteractEvent>().add_systems(
(
update_hit_result_component.after(clamp_look_direction),
handle_block_interact_event,
)
.before(handle_send_packet_event)
.chain(),
);
}
}
impl Client {
/// Right click a block. The behavior of this depends on the target block,
/// and it'll either place the block you're holding in your hand or use the
/// block you clicked (like toggling a lever).
///
/// Note that this may trigger anticheats as it doesn't take into account
/// whether you're actually looking at the block.
pub fn block_interact(&mut self, position: BlockPos) {
self.ecs.lock().send_event(BlockInteractEvent {
entity: self.entity,
position,
});
}
}
/// Right click a block. The behavior of this depends on the target block,
/// and it'll either place the block you're holding in your hand or use the
/// block you clicked (like toggling a lever).
pub struct BlockInteractEvent {
/// The local player entity that's opening the container.
pub entity: Entity,
/// The coordinates of the container.
pub position: BlockPos,
}
/// A component that contains the number of changes this client has made to
/// blocks.
#[derive(Component, Copy, Clone, Debug, Default, Deref, DerefMut)]
pub struct CurrentSequenceNumber(u32);
/// A component that contains the block that the player is currently looking at.
#[derive(Component, Clone, Debug, Deref, DerefMut)]
pub struct HitResultComponent(BlockHitResult);
fn handle_block_interact_event(
mut events: EventReader<BlockInteractEvent>,
mut query: Query<(
&LocalPlayer,
&mut CurrentSequenceNumber,
&HitResultComponent,
)>,
) {
for event in events.iter() {
let Ok((local_player, mut sequence_number, hit_result)) = query.get_mut(event.entity) else {
warn!("Sent BlockInteractEvent for entity that isn't LocalPlayer");
continue;
};
// TODO: check to make sure we're within the world border
**sequence_number += 1;
// minecraft also does the interaction client-side (so it looks like clicking a
// button is instant) but we don't really need that
// the block_hit data will depend on whether we're looking at the block and
// whether we can reach it
let block_hit = if hit_result.block_pos == event.position {
// we're looking at the block :)
BlockHit {
block_pos: hit_result.block_pos,
direction: hit_result.direction,
location: hit_result.location,
inside: hit_result.inside,
}
} else {
// we're not looking at the block, so make up some numbers
BlockHit {
block_pos: event.position,
direction: Direction::Up,
location: event.position.center(),
inside: false,
}
};
local_player.write_packet(
ServerboundUseItemOnPacket {
hand: InteractionHand::MainHand,
block_hit,
sequence: sequence_number.0,
}
.get(),
)
}
}
#[allow(clippy::type_complexity)]
fn update_hit_result_component(
mut commands: Commands,
mut query: Query<(
Entity,
Option<&mut HitResultComponent>,
&LocalGameMode,
&Position,
&EyeHeight,
&LookDirection,
&WorldName,
)>,
instance_container: Res<InstanceContainer>,
) {
for (entity, hit_result_ref, game_mode, position, eye_height, look_direction, world_name) in
&mut query
{
let pick_range = if game_mode.current == GameMode::Creative {
6.
} else {
4.5
};
let eye_position = Vec3 {
x: position.x,
y: position.y + **eye_height as f64,
z: position.z,
};
let hit_result = pick(
look_direction,
&eye_position,
world_name,
&instance_container,
pick_range,
);
if let Some(mut hit_result_ref) = hit_result_ref {
**hit_result_ref = hit_result;
} else {
commands
.entity(entity)
.insert(HitResultComponent(hit_result));
}
}
}
/// Get the block that a player would be looking at if their eyes were at the
/// given direction and position.
///
/// If you need to get the block the player is looking at right now, use
/// [`HitResultComponent`].
pub fn pick(
look_direction: &LookDirection,
eye_position: &Vec3,
world_name: &WorldName,
instance_container: &InstanceContainer,
pick_range: f64,
) -> BlockHitResult {
let view_vector = view_vector(look_direction);
let end_position = eye_position + &(view_vector * pick_range);
let instance_lock = instance_container
.get(world_name)
.expect("entities must always be in a valid world");
let instance = instance_lock.read();
azalea_physics::clip::clip(
&instance.chunks,
ClipContext {
from: *eye_position,
to: end_position,
block_shape_type: BlockShapeType::Outline,
fluid_pick_type: FluidPickType::None,
},
)
}

View file

@ -0,0 +1,721 @@
use std::collections::{HashMap, HashSet};
use azalea_chat::FormattedText;
pub use azalea_inventory::*;
use azalea_inventory::{
item::MaxStackSizeExt,
operations::{
ClickOperation, CloneClick, PickupAllClick, PickupClick, QuickCraftKind, QuickCraftStatus,
QuickCraftStatusKind, QuickMoveClick, ThrowClick,
},
};
use azalea_protocol::packets::game::{
serverbound_container_click_packet::ServerboundContainerClickPacket,
serverbound_container_close_packet::ServerboundContainerClosePacket,
};
use azalea_registry::MenuKind;
use bevy_app::{App, Plugin};
use bevy_ecs::{
component::Component,
entity::Entity,
event::EventReader,
prelude::EventWriter,
schedule::{IntoSystemConfig, IntoSystemConfigs},
system::Query,
};
use log::warn;
use crate::{client::PlayerAbilities, local_player::handle_send_packet_event, Client, LocalPlayer};
pub struct InventoryPlugin;
impl Plugin for InventoryPlugin {
fn build(&self, app: &mut App) {
app.add_event::<ClientSideCloseContainerEvent>()
.add_event::<MenuOpenedEvent>()
.add_event::<CloseContainerEvent>()
.add_event::<ContainerClickEvent>()
.add_event::<SetContainerContentEvent>()
.add_systems(
(
handle_menu_opened_event,
handle_set_container_content_event,
handle_container_click_event,
handle_container_close_event.before(handle_send_packet_event),
handle_client_side_close_container_event,
)
.chain(),
);
}
}
impl Client {
/// Return the menu that is currently open. If no menu is open, this will
/// have the player's inventory.
pub fn menu(&self) -> Menu {
let mut ecs = self.ecs.lock();
let inventory = self.query::<&InventoryComponent>(&mut ecs);
inventory.menu().clone()
}
}
/// A component present on all local players that have an inventory.
#[derive(Component, Debug)]
pub struct InventoryComponent {
/// A component that contains the player's inventory menu. This is
/// guaranteed to be a `Menu::Player`.
///
/// We keep it as a [`Menu`] since `Menu` has some useful functions that
/// bare [`azalea_inventory::Player`] doesn't have.
pub inventory_menu: azalea_inventory::Menu,
/// The ID of the container that's currently open. Its value is not
/// guaranteed to be anything specific, and may change every time you open a
/// container (unless it's 0, in which case it means that no container is
/// open).
pub id: u8,
/// The current container menu that the player has open. If no container is
/// open, this will be `None`.
pub container_menu: Option<azalea_inventory::Menu>,
/// The item that is currently held by the cursor. `Slot::Empty` if nothing
/// is currently being held.
pub carried: ItemSlot,
/// An identifier used by the server to track client inventory desyncs. This
/// is sent on every container click, and it's only ever updated when the
/// server sends a new container update.
pub state_id: u32,
pub quick_craft_status: QuickCraftStatusKind,
pub quick_craft_kind: QuickCraftKind,
/// A set of the indexes of the slots that have been right clicked in
/// this "quick craft".
pub quick_craft_slots: HashSet<u16>,
// minecraft also has these fields, but i don't
// think they're necessary?:
// private final NonNullList<ItemStack>
// remoteSlots;
// private final IntList remoteDataSlots;
// private ItemStack remoteCarried;
}
impl InventoryComponent {
/// Returns a reference to the currently active menu. If a container is open
/// it'll return [`Self::container_menu`], otherwise
/// [`Self::inventory_menu`].
///
/// Use [`Self::menu_mut`] if you need a mutable reference.
pub fn menu(&self) -> &azalea_inventory::Menu {
if let Some(menu) = &self.container_menu {
menu
} else {
&self.inventory_menu
}
}
/// Returns a mutable reference to the currently active menu. If a container
/// is open it'll return [`Self::container_menu`], otherwise
/// [`Self::inventory_menu`].
///
/// Use [`Self::menu`] if you don't need a mutable reference.
pub fn menu_mut(&mut self) -> &mut azalea_inventory::Menu {
if let Some(menu) = &mut self.container_menu {
menu
} else {
&mut self.inventory_menu
}
}
/// Modify the inventory as if the given operation was performed on it.
pub fn simulate_click(
&mut self,
operation: &ClickOperation,
player_abilities: &PlayerAbilities,
) {
if let ClickOperation::QuickCraft(quick_craft) = operation {
let last_quick_craft_status_tmp = self.quick_craft_status.clone();
self.quick_craft_status = last_quick_craft_status_tmp.clone();
let last_quick_craft_status = last_quick_craft_status_tmp;
// no carried item, reset
if self.carried.is_empty() {
return self.reset_quick_craft();
}
// if we were starting or ending, or now we aren't ending and the status
// changed, reset
if (last_quick_craft_status == QuickCraftStatusKind::Start
|| last_quick_craft_status == QuickCraftStatusKind::End
|| self.quick_craft_status != QuickCraftStatusKind::End)
&& (self.quick_craft_status != last_quick_craft_status)
{
return self.reset_quick_craft();
}
if self.quick_craft_status == QuickCraftStatusKind::Start {
self.quick_craft_kind = quick_craft.kind.clone();
if self.quick_craft_kind == QuickCraftKind::Middle && player_abilities.instant_break
{
self.quick_craft_status = QuickCraftStatusKind::Add;
self.quick_craft_slots.clear();
} else {
self.reset_quick_craft();
}
return;
}
if let QuickCraftStatus::Add { slot } = quick_craft.status {
let slot_item = self.menu().slot(slot as usize);
if let Some(slot_item) = slot_item {
if let ItemSlot::Present(carried) = &self.carried {
// minecraft also checks slot.may_place(carried) and
// menu.can_drag_to(slot)
// but they always return true so they're not relevant for us
if can_item_quick_replace(slot_item, &self.carried, true)
&& (self.quick_craft_kind == QuickCraftKind::Right
|| carried.count as usize > self.quick_craft_slots.len())
{
self.quick_craft_slots.insert(slot);
}
}
}
return;
}
if self.quick_craft_status == QuickCraftStatusKind::End {
if !self.quick_craft_slots.is_empty() {
if self.quick_craft_slots.len() == 1 {
// if we only clicked one slot, then turn this
// QuickCraftClick into a PickupClick
let slot = *self.quick_craft_slots.iter().next().unwrap();
self.reset_quick_craft();
self.simulate_click(
&match self.quick_craft_kind {
QuickCraftKind::Left => {
PickupClick::Left { slot: Some(slot) }.into()
}
QuickCraftKind::Right => {
PickupClick::Left { slot: Some(slot) }.into()
}
QuickCraftKind::Middle => {
// idk just do nothing i guess
return;
}
},
player_abilities,
);
return;
}
let ItemSlot::Present(mut carried) = self.carried.clone() else {
// this should never happen
return self.reset_quick_craft();
};
let mut carried_count = carried.count;
let mut quick_craft_slots_iter = self.quick_craft_slots.iter();
loop {
let mut slot: &ItemSlot;
let mut slot_index: u16;
let mut item_stack: &ItemSlot;
loop {
let Some(&next_slot) = quick_craft_slots_iter.next() else {
carried.count = carried_count;
self.carried = ItemSlot::Present(carried);
return self.reset_quick_craft();
};
slot = self.menu().slot(next_slot as usize).unwrap();
slot_index = next_slot;
item_stack = &self.carried;
if slot.is_present()
&& can_item_quick_replace(slot, item_stack, true)
// this always returns true in most cases
// && slot.may_place(item_stack)
&& (
self.quick_craft_kind == QuickCraftKind::Middle
|| item_stack.count() as i32 >= self.quick_craft_slots.len() as i32
)
{
break;
}
}
// get the ItemSlotData for the slot
let ItemSlot::Present(slot) = slot else {
unreachable!("the loop above requires the slot to be present to break")
};
// if self.can_drag_to(slot) {
let mut new_carried = carried.clone();
let slot_item_count = slot.count;
get_quick_craft_slot_count(
&self.quick_craft_slots,
&self.quick_craft_kind,
&mut new_carried,
slot_item_count,
);
let max_stack_size = i8::min(
new_carried.kind.max_stack_size(),
i8::min(
new_carried.kind.max_stack_size(),
slot.kind.max_stack_size(),
),
);
if new_carried.count > max_stack_size {
new_carried.count = max_stack_size;
}
carried_count -= new_carried.count - slot_item_count;
// we have to inline self.menu_mut() here to avoid the borrow checker
// complaining
let menu = if let Some(menu) = &mut self.container_menu {
menu
} else {
&mut self.inventory_menu
};
*menu.slot_mut(slot_index as usize).unwrap() =
ItemSlot::Present(new_carried);
// }
}
}
} else {
return self.reset_quick_craft();
}
}
// the quick craft status should always be in start if we're not in quick craft
// mode
if self.quick_craft_status != QuickCraftStatusKind::Start {
return self.reset_quick_craft();
}
match operation {
// left clicking outside inventory
ClickOperation::Pickup(PickupClick::Left { slot: None }) => {
if self.carried.is_present() {
// vanilla has `player.drop`s but they're only used
// server-side
// they're included as comments here in case you want to adapt this for a server
// implementation
// player.drop(self.carried, true);
self.carried = ItemSlot::Empty;
}
}
ClickOperation::Pickup(PickupClick::Right { slot: None }) => {
if self.carried.is_present() {
let _item = self.carried.split(1);
// player.drop(item, true);
}
}
ClickOperation::Pickup(
PickupClick::Left { slot: Some(slot) } | PickupClick::Right { slot: Some(slot) },
) => {
let Some(slot_item) = self.menu().slot(*slot as usize) else {
return;
};
let carried = &self.carried;
// vanilla does a check called tryItemClickBehaviourOverride
// here
// i don't understand it so i didn't implement it
match slot_item {
ItemSlot::Empty => if carried.is_present() {},
ItemSlot::Present(_) => todo!(),
}
}
ClickOperation::QuickMove(
QuickMoveClick::Left { slot } | QuickMoveClick::Right { slot },
) => {
// in vanilla it also tests if QuickMove has a slot index of -999
// but i don't think that's ever possible so it's not covered here
loop {
let new_slot_item = self.menu_mut().quick_move_stack(*slot as usize);
let slot_item = self.menu().slot(*slot as usize).unwrap();
if new_slot_item.is_empty() || slot_item != &new_slot_item {
break;
}
}
}
ClickOperation::Swap(s) => {
let source_slot_index = s.source_slot as usize;
let target_slot_index = s.target_slot as usize;
let Some(source_slot) = self.menu().slot(source_slot_index) else {
return;
};
let Some(target_slot) = self.menu().slot(target_slot_index) else {
return;
};
if source_slot.is_empty() && target_slot.is_empty() {
return;
}
if target_slot.is_empty() {
if self.menu().may_pickup(source_slot_index) {
let source_slot = source_slot.clone();
let target_slot = self.menu_mut().slot_mut(target_slot_index).unwrap();
*target_slot = source_slot;
}
} else if source_slot.is_empty() {
let ItemSlot::Present(target_item) = target_slot else {
unreachable!("target slot is not empty but is not present");
};
if self.menu().may_place(source_slot_index, target_item) {
// get the target_item but mutable
let source_max_stack_size = self.menu().max_stack_size(source_slot_index);
let target_slot = self.menu_mut().slot_mut(target_slot_index).unwrap();
let new_source_slot = target_slot.split(source_max_stack_size);
*self.menu_mut().slot_mut(source_slot_index).unwrap() = new_source_slot;
}
} else if self.menu().may_pickup(source_slot_index) {
let ItemSlot::Present(target_item) = target_slot else {
unreachable!("target slot is not empty but is not present");
};
if self.menu().may_place(source_slot_index, target_item) {
let source_max_stack = self.menu().max_stack_size(source_slot_index);
if target_slot.count() > source_max_stack as i8 {
// if there's more than the max stack size in the target slot
let target_slot = self.menu_mut().slot_mut(target_slot_index).unwrap();
let new_source_slot = target_slot.split(source_max_stack);
*self.menu_mut().slot_mut(source_slot_index).unwrap() = new_source_slot;
// if !self.inventory_menu.add(new_source_slot) {
// player.drop(new_source_slot, true);
// }
} else {
// normal swap
let new_target_slot = source_slot.clone();
let new_source_slot = target_slot.clone();
let target_slot = self.menu_mut().slot_mut(target_slot_index).unwrap();
*target_slot = new_target_slot;
let source_slot = self.menu_mut().slot_mut(source_slot_index).unwrap();
*source_slot = new_source_slot;
}
}
}
}
ClickOperation::Clone(CloneClick { slot }) => {
if !player_abilities.instant_break || self.carried.is_present() {
return;
}
let Some(source_slot) = self.menu().slot(*slot as usize) else {
return;
};
let ItemSlot::Present(source_item) = source_slot else {
return;
};
let mut new_carried = source_item.clone();
new_carried.count = new_carried.kind.max_stack_size();
self.carried = ItemSlot::Present(new_carried);
}
ClickOperation::Throw(c) => {
if self.carried.is_present() {
return;
}
let (ThrowClick::Single { slot: slot_index }
| ThrowClick::All { slot: slot_index }) = c;
let slot_index = *slot_index as usize;
let Some(slot) = self.menu_mut().slot_mut(slot_index) else {
return;
};
let ItemSlot::Present(slot_item) = slot else {
return;
};
let dropping_count = match c {
ThrowClick::Single { .. } => 1,
ThrowClick::All { .. } => slot_item.count,
};
let _dropping = slot_item.split(dropping_count as u8);
// player.drop(dropping, true);
}
ClickOperation::PickupAll(PickupAllClick {
slot: source_slot_index,
reversed,
}) => {
let source_slot_index = *source_slot_index as usize;
let source_slot = self.menu().slot(source_slot_index).unwrap();
let target_slot = self.carried.clone();
if target_slot.is_empty()
|| (source_slot.is_present() && self.menu().may_pickup(source_slot_index))
{
return;
}
let ItemSlot::Present(target_slot_item) = &target_slot else {
unreachable!("target slot is not empty but is not present");
};
for round in 0..2 {
let iterator: Box<dyn Iterator<Item = usize>> = if *reversed {
Box::new((0..self.menu().len()).rev())
} else {
Box::new(0..self.menu().len())
};
for i in iterator {
if target_slot_item.count < target_slot_item.kind.max_stack_size() {
let checking_slot = self.menu().slot(i).unwrap();
if let ItemSlot::Present(checking_item) = checking_slot {
if can_item_quick_replace(checking_slot, &target_slot, true)
&& self.menu().may_pickup(i)
&& (round != 0
|| checking_item.count
!= checking_item.kind.max_stack_size())
{
// get the checking_slot and checking_item again but mutable
let checking_slot = self.menu_mut().slot_mut(i).unwrap();
let taken_item =
checking_slot.split(checking_slot.count() as u8);
// now extend the carried item
let target_slot = &mut self.carried;
let ItemSlot::Present(target_slot_item) = target_slot else {
unreachable!("target slot is not empty but is not present");
};
target_slot_item.count += taken_item.count();
}
}
}
}
}
}
_ => {}
}
}
fn reset_quick_craft(&mut self) {
self.quick_craft_status = QuickCraftStatusKind::Start;
self.quick_craft_slots.clear();
}
}
fn can_item_quick_replace(
target_slot: &ItemSlot,
item: &ItemSlot,
ignore_item_count: bool,
) -> bool {
let ItemSlot::Present(target_slot) = target_slot else {
return false;
};
let ItemSlot::Present(item) = item else {
// i *think* this is what vanilla does
// not 100% sure lol probably doesn't matter though
return false;
};
if !item.is_same_item_and_nbt(target_slot) {
return false;
}
let count = target_slot.count as u16
+ if ignore_item_count {
0
} else {
item.count as u16
};
count <= item.kind.max_stack_size() as u16
}
// public static void getQuickCraftSlotCount(Set<Slot> quickCraftSlots, int
// quickCraftType, ItemStack itemStack, int var3) {
// switch (quickCraftType) {
// case 0:
// itemStack.setCount(Mth.floor((float) itemStack.getCount() / (float)
// quickCraftSlots.size())); break;
// case 1:
// itemStack.setCount(1);
// break;
// case 2:
// itemStack.setCount(itemStack.getItem().getMaxStackSize());
// }
// itemStack.grow(var3);
// }
fn get_quick_craft_slot_count(
quick_craft_slots: &HashSet<u16>,
quick_craft_kind: &QuickCraftKind,
item: &mut ItemSlotData,
slot_item_count: i8,
) {
item.count = match quick_craft_kind {
QuickCraftKind::Left => item.count / quick_craft_slots.len() as i8,
QuickCraftKind::Right => 1,
QuickCraftKind::Middle => item.kind.max_stack_size(),
};
item.count += slot_item_count;
}
impl Default for InventoryComponent {
fn default() -> Self {
InventoryComponent {
inventory_menu: Menu::Player(azalea_inventory::Player::default()),
id: 0,
container_menu: None,
carried: ItemSlot::Empty,
state_id: 0,
quick_craft_status: QuickCraftStatusKind::Start,
quick_craft_kind: QuickCraftKind::Middle,
quick_craft_slots: HashSet::new(),
}
}
}
/// Sent from the server when a menu (like a chest or crafting table) was
/// opened by the client.
#[derive(Debug)]
pub struct MenuOpenedEvent {
pub entity: Entity,
pub window_id: u32,
pub menu_type: MenuKind,
pub title: FormattedText,
}
fn handle_menu_opened_event(
mut events: EventReader<MenuOpenedEvent>,
mut query: Query<&mut InventoryComponent>,
) {
for event in events.iter() {
let mut inventory = query.get_mut(event.entity).unwrap();
inventory.id = event.window_id as u8;
inventory.container_menu = Some(Menu::from_kind(event.menu_type));
}
}
/// Tell the server that we want to close a container.
///
/// Note that this is also sent when the client closes its own inventory, even
/// though there is no packet for opening its inventory.
pub struct CloseContainerEvent {
pub entity: Entity,
/// The ID of the container to close. 0 for the player's inventory. If this
/// is not the same as the currently open inventory, nothing will happen.
pub id: u8,
}
fn handle_container_close_event(
mut events: EventReader<CloseContainerEvent>,
mut client_side_events: EventWriter<ClientSideCloseContainerEvent>,
query: Query<(&LocalPlayer, &InventoryComponent)>,
) {
for event in events.iter() {
let (local_player, inventory) = query.get(event.entity).unwrap();
if event.id != inventory.id {
warn!(
"Tried to close container with ID {}, but the current container ID is {}",
event.id, inventory.id
);
continue;
}
local_player.write_packet(
ServerboundContainerClosePacket {
container_id: inventory.id,
}
.get(),
);
client_side_events.send(ClientSideCloseContainerEvent {
entity: event.entity,
});
}
}
/// Close a container without notifying the server.
///
/// Note that this also gets fired when we get a [`CloseContainerEvent`].
pub struct ClientSideCloseContainerEvent {
pub entity: Entity,
}
fn handle_client_side_close_container_event(
mut events: EventReader<ClientSideCloseContainerEvent>,
mut query: Query<&mut InventoryComponent>,
) {
for event in events.iter() {
let mut inventory = query.get_mut(event.entity).unwrap();
inventory.container_menu = None;
inventory.id = 0;
}
}
#[derive(Debug)]
pub struct ContainerClickEvent {
pub entity: Entity,
pub window_id: u8,
pub operation: ClickOperation,
}
fn handle_container_click_event(
mut events: EventReader<ContainerClickEvent>,
mut query: Query<(&mut InventoryComponent, &LocalPlayer)>,
) {
for event in events.iter() {
let (mut inventory, local_player) = query.get_mut(event.entity).unwrap();
if inventory.id != event.window_id {
warn!(
"Tried to click container with ID {}, but the current container ID is {}",
event.window_id, inventory.id
);
continue;
}
let menu = inventory.menu_mut();
let old_slots = menu.slots().clone();
// menu.click(&event.operation);
// see which slots changed after clicking and put them in the hashmap
// the server uses this to check if we desynced
let mut changed_slots: HashMap<u16, ItemSlot> = HashMap::new();
for (slot_index, old_slot) in old_slots.iter().enumerate() {
let new_slot = &menu.slots()[slot_index];
if old_slot != new_slot {
changed_slots.insert(slot_index as u16, new_slot.clone());
}
}
local_player.write_packet(
ServerboundContainerClickPacket {
container_id: event.window_id,
state_id: inventory.state_id,
slot_num: event.operation.slot_num().map(|n| n as i16).unwrap_or(-999),
button_num: event.operation.button_num(),
click_type: event.operation.click_type(),
changed_slots,
carried_item: inventory.carried.clone(),
}
.get(),
)
}
}
/// Sent from the server when the contents of a container are replaced. Usually
/// triggered by the `ContainerSetContent` packet.
pub struct SetContainerContentEvent {
pub entity: Entity,
pub slots: Vec<ItemSlot>,
pub container_id: u8,
}
fn handle_set_container_content_event(
mut events: EventReader<SetContainerContentEvent>,
mut query: Query<&mut InventoryComponent>,
) {
for event in events.iter() {
let mut inventory = query.get_mut(event.entity).unwrap();
if event.container_id != inventory.id {
warn!(
"Tried to set container content with ID {}, but the current container ID is {}",
event.container_id, inventory.id
);
continue;
}
let menu = inventory.menu_mut();
for (i, slot) in event.slots.iter().enumerate() {
if let Some(slot_mut) = menu.slot_mut(i) {
*slot_mut = slot.clone();
}
}
}
}

View file

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

View file

@ -1,14 +1,18 @@
use std::{io, sync::Arc};
use azalea_auth::game_profile::GameProfile;
use azalea_core::ChunkPos;
use azalea_core::{ChunkPos, GameMode};
use azalea_protocol::packets::game::ServerboundGamePacket;
use azalea_world::{
entity::{self, Dead},
Instance, PartialInstance,
entity::{self, Dead, WorldName},
Instance, InstanceContainer, PartialInstance,
};
use bevy_ecs::{
component::Component, entity::Entity, event::EventReader, query::Added, system::Query,
component::Component,
entity::Entity,
event::EventReader,
query::Added,
system::{Query, Res},
};
use derive_more::{Deref, DerefMut};
use parking_lot::RwLock;
@ -75,9 +79,17 @@ pub struct GameProfileComponent(pub GameProfile);
/// Marks a [`LocalPlayer`] that's in a loaded chunk. This is updated at the
/// beginning of every tick.
#[derive(Component)]
#[derive(Component, Clone, Debug, Copy)]
pub struct LocalPlayerInLoadedChunk;
/// The gamemode of a local player. For a non-local player, you can look up the
/// player in the [`TabList`].
#[derive(Component, Clone, Debug, Copy)]
pub struct LocalGameMode {
pub current: GameMode,
pub previous: Option<GameMode>,
}
impl LocalPlayer {
/// Create a new `LocalPlayer`.
pub fn new(
@ -104,7 +116,7 @@ impl LocalPlayer {
}
/// Write a packet directly to the server.
pub fn write_packet(&mut self, packet: ServerboundGamePacket) {
pub fn write_packet(&self, packet: ServerboundGamePacket) {
self.packet_writer
.send(packet)
.expect("write_packet shouldn't be able to be called if the connection is closed");
@ -122,16 +134,15 @@ impl Drop for LocalPlayer {
/// Update the [`LocalPlayerInLoadedChunk`] component for all [`LocalPlayer`]s.
pub fn update_in_loaded_chunk(
mut commands: bevy_ecs::system::Commands,
query: Query<(Entity, &LocalPlayer, &entity::Position)>,
query: Query<(Entity, &WorldName, &entity::Position)>,
instance_container: Res<InstanceContainer>,
) {
for (entity, local_player, position) in &query {
let player_chunk_pos = ChunkPos::from(position);
let in_loaded_chunk = local_player
.world
.read()
.chunks
.get(&player_chunk_pos)
.is_some();
let instance_lock = instance_container
.get(local_player)
.expect("local player should always be in an instance");
let in_loaded_chunk = instance_lock.read().chunks.get(&player_chunk_pos).is_some();
if in_loaded_chunk {
commands.entity(entity).insert(LocalPlayerInLoadedChunk);
} else {
@ -176,7 +187,7 @@ pub fn handle_send_packet_event(
mut query: Query<&mut LocalPlayer>,
) {
for event in send_packet_events.iter() {
if let Ok(mut local_player) = query.get_mut(event.entity) {
if let Ok(local_player) = query.get_mut(event.entity) {
local_player.write_packet(event.packet.clone());
}
}

View file

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

View file

@ -1,6 +1,6 @@
use std::{collections::HashSet, io::Cursor, sync::Arc};
use azalea_core::{ChunkPos, ResourceLocation, Vec3};
use azalea_core::{ChunkPos, GameMode, ResourceLocation, Vec3};
use azalea_protocol::{
connect::{ReadConnection, WriteConnection},
packets::game::{
@ -16,7 +16,7 @@ use azalea_protocol::{
use azalea_world::{
entity::{
metadata::{apply_metadata, Health, PlayerMetadataBundle},
set_rotation, Dead, EntityBundle, EntityKind, EntityUpdateSet, LastSentPosition,
Dead, EntityBundle, EntityKind, EntityUpdateSet, LastSentPosition, LookDirection,
MinecraftEntityId, Physics, PlayerBundle, Position, WorldName,
},
entity::{LoadedBy, RelativeEntityUpdate},
@ -37,9 +37,13 @@ use tokio::sync::mpsc;
use crate::{
chat::{ChatPacket, ChatReceivedEvent},
client::TabList,
client::{PlayerAbilities, TabList},
disconnect::DisconnectEvent,
local_player::{GameProfileComponent, LocalPlayer},
inventory::{
ClientSideCloseContainerEvent, InventoryComponent, MenuOpenedEvent,
SetContainerContentEvent,
},
local_player::{GameProfileComponent, LocalGameMode, LocalPlayer},
ClientInformation, PlayerInfo,
};
@ -194,7 +198,7 @@ fn process_packet_events(ecs: &mut World) {
)>,
ResMut<InstanceContainer>,
)> = SystemState::new(ecs);
let (mut commands, mut query, mut world_container) = system_state.get_mut(ecs);
let (mut commands, mut query, mut instance_container) = system_state.get_mut(ecs);
let (mut local_player, world_name, game_profile, client_information) =
query.get_mut(player_entity).unwrap();
@ -220,16 +224,16 @@ fn process_packet_events(ecs: &mut World) {
.entity(player_entity)
.insert(WorldName(new_world_name.clone()));
}
// add this world to the world_container (or don't if it's already
// add this world to the instance_container (or don't if it's already
// there)
let weak_world = world_container.insert(
let weak_world = instance_container.insert(
new_world_name.clone(),
dimension.height,
dimension.min_y,
);
// set the partial_world to an empty world
// (when we add chunks or entities those will be in the
// world_container)
// instance_container)
*local_player.partial_instance.write() = PartialInstance::new(
client_information.view_distance.into(),
@ -250,9 +254,14 @@ fn process_packet_events(ecs: &mut World) {
metadata: PlayerMetadataBundle::default(),
};
// insert our components into the ecs :)
commands
.entity(player_entity)
.insert((MinecraftEntityId(p.player_id), player_bundle));
commands.entity(player_entity).insert((
MinecraftEntityId(p.player_id),
LocalGameMode {
current: p.game_type,
previous: p.previous_game_type.into(),
},
player_bundle,
));
}
// send the client information that we have set
@ -288,6 +297,12 @@ fn process_packet_events(ecs: &mut World) {
}
ClientboundGamePacket::PlayerAbilities(p) => {
debug!("Got player abilities packet {:?}", p);
let mut system_state: SystemState<Query<&mut PlayerAbilities>> =
SystemState::new(ecs);
let mut query = system_state.get_mut(ecs);
let mut player_abilities = query.get_mut(player_entity).unwrap();
*player_abilities = PlayerAbilities::from(p);
}
ClientboundGamePacket::SetCarriedItem(p) => {
debug!("Got set carried item packet {:?}", p);
@ -319,16 +334,18 @@ fn process_packet_events(ecs: &mut World) {
// TODO: reply with teleport confirm
debug!("Got player position packet {:?}", p);
#[allow(clippy::type_complexity)]
let mut system_state: SystemState<
Query<(
&mut LocalPlayer,
&mut Physics,
&mut LookDirection,
&mut Position,
&mut LastSentPosition,
)>,
> = SystemState::new(ecs);
let mut query = system_state.get_mut(ecs);
let Ok((mut local_player, mut physics, mut position, mut last_sent_position)) =
let Ok((local_player, mut physics, mut direction, mut position, mut last_sent_position)) =
query.get_mut(player_entity) else {
continue;
};
@ -364,10 +381,10 @@ fn process_packet_events(ecs: &mut World) {
let mut y_rot = p.y_rot;
let mut x_rot = p.x_rot;
if p.relative_arguments.x_rot {
x_rot += physics.x_rot;
x_rot += direction.x_rot;
}
if p.relative_arguments.y_rot {
y_rot += physics.y_rot;
y_rot += direction.y_rot;
}
physics.delta = Vec3 {
@ -378,7 +395,7 @@ fn process_packet_events(ecs: &mut World) {
// we call a function instead of setting the fields ourself since the
// function makes sure the rotations stay in their
// ranges
set_rotation(&mut physics, y_rot, x_rot);
(direction.y_rot, direction.x_rot) = (y_rot, x_rot);
// TODO: minecraft sets "xo", "yo", and "zo" here but idk what that means
// so investigate that ig
let new_pos = Vec3 {
@ -633,9 +650,6 @@ fn process_packet_events(ecs: &mut World) {
ClientboundGamePacket::SetDefaultSpawnPosition(p) => {
debug!("Got set default spawn position packet {:?}", p);
}
ClientboundGamePacket::ContainerSetContent(p) => {
debug!("Got container set content packet {:?}", p);
}
ClientboundGamePacket::SetHealth(p) => {
debug!("Got set health packet {:?}", p);
@ -765,7 +779,7 @@ fn process_packet_events(ecs: &mut World) {
id: p.id,
});
let mut local_player = query.get_mut(player_entity).unwrap();
let local_player = query.get_mut(player_entity).unwrap();
local_player.write_packet(ServerboundKeepAlivePacket { id: p.id }.get());
debug!("Sent keep alive packet {p:?} for {player_entity:?}");
}
@ -831,7 +845,23 @@ fn process_packet_events(ecs: &mut World) {
}
}
ClientboundGamePacket::GameEvent(p) => {
use azalea_protocol::packets::game::clientbound_game_event_packet::EventType;
debug!("Got game event packet {:?}", p);
#[allow(clippy::single_match)]
match p.event {
EventType::ChangeGameMode => {
let mut system_state: SystemState<Query<&mut LocalGameMode>> =
SystemState::new(ecs);
let mut query = system_state.get_mut(ecs);
let mut local_game_mode = query.get_mut(player_entity).unwrap();
if let Some(new_game_mode) = GameMode::from_id(p.param as u8) {
local_game_mode.current = new_game_mode;
}
}
_ => {}
}
}
ClientboundGamePacket::LevelParticles(p) => {
debug!("Got level particles packet {:?}", p);
@ -855,8 +885,93 @@ fn process_packet_events(ecs: &mut World) {
}
ClientboundGamePacket::BossEvent(_) => {}
ClientboundGamePacket::CommandSuggestions(_) => {}
ClientboundGamePacket::ContainerSetData(_) => {}
ClientboundGamePacket::ContainerSetSlot(_) => {}
ClientboundGamePacket::ContainerSetContent(p) => {
debug!("Got container set content packet {:?}", p);
let mut system_state: SystemState<(
Query<&mut InventoryComponent>,
EventWriter<SetContainerContentEvent>,
)> = SystemState::new(ecs);
let (mut query, mut events) = system_state.get_mut(ecs);
let mut inventory = query.get_mut(player_entity).unwrap();
// container id 0 is always the player's inventory
if p.container_id == 0 {
// this is just so it has the same type as the `else` block
for (i, slot) in p.items.iter().enumerate() {
if let Some(slot_mut) = inventory.inventory_menu.slot_mut(i) {
*slot_mut = slot.clone();
}
}
} else {
events.send(SetContainerContentEvent {
entity: player_entity,
slots: p.items.clone(),
container_id: p.container_id as u8,
});
}
}
ClientboundGamePacket::ContainerSetData(p) => {
debug!("Got container set data packet {:?}", p);
// let mut system_state: SystemState<Query<&mut
// InventoryComponent>> =
// SystemState::new(ecs);
// let mut query = system_state.get_mut(ecs);
// let mut inventory =
// query.get_mut(player_entity).unwrap();
// TODO: handle ContainerSetData packet
// this is used for various things like the furnace progress
// bar
// see https://wiki.vg/Protocol#Set_Container_Property
}
ClientboundGamePacket::ContainerSetSlot(p) => {
debug!("Got container set slot packet {:?}", p);
let mut system_state: SystemState<Query<&mut InventoryComponent>> =
SystemState::new(ecs);
let mut query = system_state.get_mut(ecs);
let mut inventory = query.get_mut(player_entity).unwrap();
if p.container_id == -1 {
// -1 means carried item
inventory.carried = p.item_stack.clone();
} else if p.container_id == -2 {
if let Some(slot) = inventory.inventory_menu.slot_mut(p.slot.into()) {
*slot = p.item_stack.clone();
}
} else {
let is_creative_mode_and_inventory_closed = false;
// technically minecraft has slightly different behavior here if you're in
// creative mode and have your inventory open
if p.container_id == 0
&& azalea_inventory::Player::is_hotbar_slot(p.slot.into())
{
// minecraft also sets a "pop time" here which is used for an animation
// but that's not really necessary
if let Some(slot) = inventory.inventory_menu.slot_mut(p.slot.into()) {
*slot = p.item_stack.clone();
}
} else if p.container_id == (inventory.id as i8)
&& (p.container_id != 0 || !is_creative_mode_and_inventory_closed)
{
// var2.containerMenu.setItem(var4, var1.getStateId(), var3);
if let Some(slot) = inventory.menu_mut().slot_mut(p.slot.into()) {
*slot = p.item_stack.clone();
inventory.state_id = p.state_id;
}
}
}
}
ClientboundGamePacket::ContainerClose(_p) => {
// there's p.container_id but minecraft doesn't actually check it
let mut system_state: SystemState<EventWriter<ClientSideCloseContainerEvent>> =
SystemState::new(ecs);
let mut client_side_close_container_events = system_state.get_mut(ecs);
client_side_close_container_events.send(ClientSideCloseContainerEvent {
entity: player_entity,
})
}
ClientboundGamePacket::Cooldown(_) => {}
ClientboundGamePacket::CustomChatCompletions(_) => {}
ClientboundGamePacket::DeleteChat(_) => {}
@ -867,7 +982,18 @@ fn process_packet_events(ecs: &mut World) {
ClientboundGamePacket::MerchantOffers(_) => {}
ClientboundGamePacket::MoveVehicle(_) => {}
ClientboundGamePacket::OpenBook(_) => {}
ClientboundGamePacket::OpenScreen(_) => {}
ClientboundGamePacket::OpenScreen(p) => {
debug!("Got open screen packet {:?}", p);
let mut system_state: SystemState<EventWriter<MenuOpenedEvent>> =
SystemState::new(ecs);
let mut menu_opened_events = system_state.get_mut(ecs);
menu_opened_events.send(MenuOpenedEvent {
entity: player_entity,
window_id: p.container_id,
menu_type: p.menu_type,
title: p.title,
})
}
ClientboundGamePacket::OpenSignEditor(_) => {}
ClientboundGamePacket::Ping(_) => {}
ClientboundGamePacket::PlaceGhostRecipe(_) => {}
@ -935,7 +1061,6 @@ fn process_packet_events(ecs: &mut World) {
ClientboundGamePacket::TakeItemEntity(_) => {}
ClientboundGamePacket::DisguisedChat(_) => {}
ClientboundGamePacket::UpdateEnabledFeatures(_) => {}
ClientboundGamePacket::ContainerClose(_) => {}
ClientboundGamePacket::Bundle(_) => {}
ClientboundGamePacket::DamageEvent(_) => {}
ClientboundGamePacket::HurtAnimation(_) => {}

View file

@ -1,6 +1,6 @@
use azalea_auth::game_profile::GameProfile;
use azalea_chat::FormattedText;
use azalea_core::GameType;
use azalea_core::GameMode;
use azalea_world::entity::EntityInfos;
use bevy_ecs::{
event::EventReader,
@ -18,7 +18,10 @@ pub struct PlayerInfo {
pub profile: GameProfile,
/// The player's UUID.
pub uuid: Uuid,
pub gamemode: GameType,
/// The current gamemode of the player, like survival or creative.
pub gamemode: GameMode,
/// The player's latency in milliseconds. The bars in the tab screen depend
/// on this.
pub latency: i32,
/// The player's display name in the tab list, but only if it's different
/// from the player's normal username. Use `player_info.profile.name` to get

4
azalea-core/Cargo.toml Normal file → Executable file
View file

@ -10,8 +10,12 @@ version = "0.6.0"
[dependencies]
azalea-buf = { path = "../azalea-buf", version = "^0.6.0" }
azalea-chat = { path = "../azalea-chat", version = "^0.6.0" }
azalea-inventory = { version = "0.1.0", path = "../azalea-inventory" }
azalea-nbt = { path = "../azalea-nbt", version = "^0.6.0" }
azalea-registry = { path = "../azalea-registry", version = "^0.6.0" }
bevy_ecs = { version = "0.10.0", default-features = false, optional = true }
num-traits = "0.2.15"
serde = { version = "^1.0", optional = true }
uuid = "^1.1.2"

View file

@ -164,15 +164,15 @@ impl AABB {
}
}
pub fn move_relative(&self, x: f64, y: f64, z: f64) -> AABB {
pub fn move_relative(&self, delta: &Vec3) -> AABB {
AABB {
min_x: self.min_x + x,
min_y: self.min_y + y,
min_z: self.min_z + z,
min_x: self.min_x + delta.x,
min_y: self.min_y + delta.y,
min_z: self.min_z + delta.z,
max_x: self.max_x + x,
max_y: self.max_y + y,
max_z: self.max_z + z,
max_x: self.max_x + delta.x,
max_y: self.max_y + delta.y,
max_z: self.max_z + delta.z,
}
}
@ -227,12 +227,11 @@ impl AABB {
pub fn clip(&self, min: &Vec3, max: &Vec3) -> Option<Vec3> {
let mut t = 1.0;
let delta = max - min;
let _dir = self.get_direction(self, min, &mut t, None, &delta)?;
let _dir = Self::get_direction(self, min, &mut t, None, &delta)?;
Some(min + &(delta * t))
}
pub fn clip_iterable(
&self,
boxes: &Vec<AABB>,
from: &Vec3,
to: &Vec3,
@ -243,7 +242,13 @@ impl AABB {
let delta = to - from;
for aabb in boxes {
dir = self.get_direction(aabb, from, &mut t, dir, &delta);
dir = Self::get_direction(
&aabb.move_relative(&pos.to_vec3_floored()),
from,
&mut t,
dir,
&delta,
);
}
let dir = dir?;
Some(BlockHitResult {
@ -256,15 +261,14 @@ impl AABB {
}
fn get_direction(
&self,
aabb: &AABB,
from: &Vec3,
t: &mut f64,
dir: Option<Direction>,
mut dir: Option<Direction>,
delta: &Vec3,
) -> Option<Direction> {
if delta.x > EPSILON {
return self.clip_point(ClipPointOpts {
dir = Self::clip_point(ClipPointOpts {
t,
approach_dir: dir,
delta,
@ -277,7 +281,7 @@ impl AABB {
start: from,
});
} else if delta.x < -EPSILON {
return self.clip_point(ClipPointOpts {
dir = Self::clip_point(ClipPointOpts {
t,
approach_dir: dir,
delta,
@ -292,7 +296,7 @@ impl AABB {
}
if delta.y > EPSILON {
return self.clip_point(ClipPointOpts {
dir = Self::clip_point(ClipPointOpts {
t,
approach_dir: dir,
delta: &Vec3 {
@ -313,7 +317,7 @@ impl AABB {
},
});
} else if delta.y < -EPSILON {
return self.clip_point(ClipPointOpts {
dir = Self::clip_point(ClipPointOpts {
t,
approach_dir: dir,
delta: &Vec3 {
@ -336,7 +340,7 @@ impl AABB {
}
if delta.z > EPSILON {
return self.clip_point(ClipPointOpts {
dir = Self::clip_point(ClipPointOpts {
t,
approach_dir: dir,
delta: &Vec3 {
@ -357,7 +361,7 @@ impl AABB {
},
});
} else if delta.z < -EPSILON {
return self.clip_point(ClipPointOpts {
dir = Self::clip_point(ClipPointOpts {
t,
approach_dir: dir,
delta: &Vec3 {
@ -382,18 +386,18 @@ impl AABB {
dir
}
fn clip_point(&self, opts: ClipPointOpts) -> Option<Direction> {
let t_x = (opts.begin - opts.start.x) / opts.delta.x;
let t_y = (opts.start.y + t_x) / opts.delta.y;
let t_z = (opts.start.z + t_x) / opts.delta.z;
if 0.0 < t_x
&& t_x < *opts.t
&& opts.min_x - EPSILON < t_y
&& t_y < opts.max_x + EPSILON
&& opts.min_z - EPSILON < t_z
&& t_z < opts.max_z + EPSILON
fn clip_point(opts: ClipPointOpts) -> Option<Direction> {
let d = (opts.begin - opts.start.x) / opts.delta.x;
let e = opts.start.y + d * opts.delta.y;
let f = opts.start.z + d * opts.delta.z;
if 0.0 < d
&& d < *opts.t
&& opts.min_x - EPSILON < e
&& e < opts.max_x + EPSILON
&& opts.min_z - EPSILON < f
&& f < opts.max_z + EPSILON
{
*opts.t = t_x;
*opts.t = d;
Some(opts.result_dir)
} else {
opts.approach_dir
@ -435,3 +439,28 @@ impl AABB {
axis.choose(self.min_x, self.min_y, self.min_z)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_aabb_clip_iterable() {
assert_ne!(
AABB::clip_iterable(
&vec![AABB {
min_x: 0.,
min_y: 0.,
min_z: 0.,
max_x: 1.,
max_y: 1.,
max_z: 1.,
}],
&Vec3::new(-1., -1., -1.),
&Vec3::new(1., 1., 1.),
&BlockPos::new(0, 0, 0),
),
None
);
}
}

View file

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

View file

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

View file

@ -1,8 +1,9 @@
use azalea_buf::{BufReadError, McBufReadable, McBufWritable};
use std::io::{Cursor, Write};
#[derive(Hash, Copy, Clone, Debug, Default)]
pub enum GameType {
/// A Minecraft gamemode, like survival or creative.
#[derive(Hash, Copy, Clone, Debug, Default, Eq, PartialEq)]
pub enum GameMode {
#[default]
Survival,
Creative,
@ -10,30 +11,30 @@ pub enum GameType {
Spectator,
}
impl GameType {
impl GameMode {
pub fn to_id(&self) -> u8 {
match self {
GameType::Survival => 0,
GameType::Creative => 1,
GameType::Adventure => 2,
GameType::Spectator => 3,
GameMode::Survival => 0,
GameMode::Creative => 1,
GameMode::Adventure => 2,
GameMode::Spectator => 3,
}
}
/// Get the id of the game type, but return -1 if the game type is invalid.
pub fn to_optional_id<T: Into<Option<GameType>>>(game_type: T) -> i8 {
pub fn to_optional_id<T: Into<Option<GameMode>>>(game_type: T) -> i8 {
match game_type.into() {
Some(game_type) => game_type.to_id() as i8,
None => -1,
}
}
pub fn from_id(id: u8) -> Option<GameType> {
pub fn from_id(id: u8) -> Option<GameMode> {
Some(match id {
0 => GameType::Survival,
1 => GameType::Creative,
2 => GameType::Adventure,
3 => GameType::Spectator,
0 => GameMode::Survival,
1 => GameMode::Creative,
2 => GameMode::Adventure,
3 => GameMode::Spectator,
_ => return None,
})
}
@ -42,7 +43,7 @@ impl GameType {
Some(
match id {
-1 => None,
id => Some(GameType::from_id(id as u8)?),
id => Some(GameMode::from_id(id as u8)?),
}
.into(),
)
@ -52,10 +53,10 @@ impl GameType {
// TODO: these should be translated
// TranslatableComponent("selectWorld.gameMode." + string2)
match self {
GameType::Survival => "Survival",
GameType::Creative => "Creative",
GameType::Adventure => "Adventure",
GameType::Spectator => "Spectator",
GameMode::Survival => "Survival",
GameMode::Creative => "Creative",
GameMode::Adventure => "Adventure",
GameMode::Spectator => "Spectator",
}
}
@ -63,32 +64,32 @@ impl GameType {
// TODO: These should be translated TranslatableComponent("gameMode." +
// string2);
match self {
GameType::Survival => "Survival Mode",
GameType::Creative => "Creative Mode",
GameType::Adventure => "Adventure Mode",
GameType::Spectator => "Spectator Mode",
GameMode::Survival => "Survival Mode",
GameMode::Creative => "Creative Mode",
GameMode::Adventure => "Adventure Mode",
GameMode::Spectator => "Spectator Mode",
}
}
pub fn from_name(name: &str) -> GameType {
pub fn from_name(name: &str) -> GameMode {
match name {
"survival" => GameType::Survival,
"creative" => GameType::Creative,
"adventure" => GameType::Adventure,
"spectator" => GameType::Spectator,
"survival" => GameMode::Survival,
"creative" => GameMode::Creative,
"adventure" => GameMode::Adventure,
"spectator" => GameMode::Spectator,
_ => panic!("Unknown game type name: {name}"),
}
}
}
impl McBufReadable for GameType {
impl McBufReadable for GameMode {
fn read_from(buf: &mut Cursor<&[u8]>) -> Result<Self, BufReadError> {
let id = u8::read_from(buf)?;
GameType::from_id(id).ok_or(BufReadError::UnexpectedEnumVariant { id: id as i32 })
GameMode::from_id(id).ok_or(BufReadError::UnexpectedEnumVariant { id: id as i32 })
}
}
impl McBufWritable for GameType {
impl McBufWritable for GameMode {
fn write_into(&self, buf: &mut impl Write) -> Result<(), std::io::Error> {
u8::write_into(&self.to_id(), buf)
}
@ -97,15 +98,15 @@ impl McBufWritable for GameType {
/// Rust doesn't let us `impl McBufReadable for Option<GameType>` so we have to
/// make a new type :(
#[derive(Hash, Copy, Clone, Debug)]
pub struct OptionalGameType(pub Option<GameType>);
pub struct OptionalGameType(pub Option<GameMode>);
impl From<Option<GameType>> for OptionalGameType {
fn from(game_type: Option<GameType>) -> Self {
impl From<Option<GameMode>> for OptionalGameType {
fn from(game_type: Option<GameMode>) -> Self {
OptionalGameType(game_type)
}
}
impl From<OptionalGameType> for Option<GameType> {
impl From<OptionalGameType> for Option<GameMode> {
fn from(optional_game_type: OptionalGameType) -> Self {
optional_game_type.0
}
@ -114,12 +115,12 @@ impl From<OptionalGameType> for Option<GameType> {
impl McBufReadable for OptionalGameType {
fn read_from(buf: &mut Cursor<&[u8]>) -> Result<Self, BufReadError> {
let id = i8::read_from(buf)?;
GameType::from_optional_id(id).ok_or(BufReadError::UnexpectedEnumVariant { id: id as i32 })
GameMode::from_optional_id(id).ok_or(BufReadError::UnexpectedEnumVariant { id: id as i32 })
}
}
impl McBufWritable for OptionalGameType {
fn write_into(&self, buf: &mut impl Write) -> Result<(), std::io::Error> {
GameType::to_optional_id(*self).write_into(buf)
GameMode::to_optional_id(*self).write_into(buf)
}
}

View file

@ -13,9 +13,6 @@ pub use resource_location::*;
mod game_type;
pub use game_type::*;
mod slot;
pub use slot::*;
mod position;
pub use position::*;
@ -40,6 +37,8 @@ pub use aabb::*;
mod block_hit_result;
pub use block_hit_result::*;
// some random math things used in minecraft are defined down here
// TODO: make this generic
pub fn binary_search(mut min: i32, max: i32, predicate: &dyn Fn(i32) -> bool) -> i32 {
let mut diff = max - min;
@ -70,6 +69,10 @@ pub fn gcd(mut a: u32, mut b: u32) -> u32 {
a
}
pub fn lerp<T: num_traits::Float>(amount: T, a: T, b: T) -> T {
a + amount * (b - a)
}
#[cfg(test)]
mod tests {
use super::*;

View file

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

View file

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

View file

@ -0,0 +1,12 @@
[package]
edition = "2021"
name = "azalea-inventory"
version = "0.1.0"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
azalea-buf = { version = "0.6.0", path = "../azalea-buf" }
azalea-inventory-macros = { version = "0.1.0", path = "./azalea-inventory-macros" }
azalea-nbt = { version = "0.6.0", path = "../azalea-nbt" }
azalea-registry = { version = "0.6.0", path = "../azalea-registry" }

View file

@ -0,0 +1,2 @@
Representations of various inventory data structures in Minecraft.

View file

@ -0,0 +1,14 @@
[package]
name = "azalea-inventory-macros"
version = "0.1.0"
edition = "2021"
[lib]
proc-macro = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
proc-macro2 = "1.0.47"
quote = "1.0.21"
syn = "1.0.104"

View file

@ -0,0 +1,45 @@
mod location_enum;
mod menu_enum;
mod menu_impl;
mod parse_macro;
mod utils;
use parse_macro::{DeclareMenus, Field};
use proc_macro::TokenStream;
use proc_macro2::Span;
use quote::quote;
use syn::{self, parse_macro_input, Ident};
#[proc_macro]
pub fn declare_menus(input: TokenStream) -> TokenStream {
let mut input = parse_macro_input!(input as DeclareMenus);
// implicitly add a `player` field at the end unless an `inventory` field
// is present
for menu in &mut input.menus {
let mut inventory_field_missing = true;
for field in &menu.fields {
if matches!(field.name.to_string().as_str(), "inventory" | "player") {
inventory_field_missing = false;
}
}
if inventory_field_missing {
menu.fields.push(Field {
name: Ident::new("player", Span::call_site()),
length: 36,
})
}
}
let menu_enum = menu_enum::generate(&input);
let menu_impl = menu_impl::generate(&input);
let location_enum = location_enum::generate(&input);
quote! {
#menu_enum
#menu_impl
#location_enum
}
.into()
}

View file

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

View file

@ -0,0 +1,70 @@
//! Generate the `enum menu` and nothing else. Implementations are in
//! impl_menu.rs
use crate::parse_macro::{DeclareMenus, Field, Menu};
use proc_macro2::TokenStream;
use quote::quote;
pub fn generate(input: &DeclareMenus) -> TokenStream {
let mut variants = quote! {};
let mut player_fields = None;
for menu in &input.menus {
if menu.name == "Player" {
player_fields = Some(generate_fields(&menu.fields, true));
} else {
variants.extend(generate_variant_for_menu(menu));
}
}
let player_fields = player_fields.expect("Player variant must be present");
quote! {
#[derive(Clone, Debug, Default)]
pub struct Player {
#player_fields
}
/// A menu, which is a fixed collection of slots.
#[derive(Clone, Debug)]
pub enum Menu {
Player(Player),
#variants
}
}
}
/// Player {
/// craft_result: ItemSlot,
/// craft: [ItemSlot; 4],
/// armor: [ItemSlot; 4],
/// inventory: [ItemSlot; 36],
/// offhand: ItemSlot,
/// },
fn generate_variant_for_menu(menu: &Menu) -> TokenStream {
let name = &menu.name;
let fields = generate_fields(&menu.fields, false);
quote! {
#name {
#fields
},
}
}
fn generate_fields(fields: &[Field], public: bool) -> TokenStream {
let mut generated_fields = quote! {};
for field in fields {
let field_length = field.length;
let field_type = if field.length == 1 {
quote! { ItemSlot }
} else {
quote! { SlotList<#field_length> }
};
let field_name = &field.name;
if public {
generated_fields.extend(quote! { pub #field_name: #field_type, })
} else {
generated_fields.extend(quote! { #field_name: #field_type, })
}
}
generated_fields
}

View file

@ -0,0 +1,448 @@
use crate::{
parse_macro::{DeclareMenus, Menu},
utils::{to_pascal_case, to_snake_case},
};
use proc_macro2::TokenStream;
use quote::quote;
use syn::Ident;
pub fn generate(input: &DeclareMenus) -> TokenStream {
let mut slot_mut_match_variants = quote! {};
let mut slot_match_variants = quote! {};
let mut len_match_variants = quote! {};
let mut kind_match_variants = quote! {};
let mut slots_match_variants = quote! {};
let mut contents_match_variants = quote! {};
let mut location_match_variants = quote! {};
let mut player_slots_range_match_variants = quote! {};
let mut player_consts = quote! {};
let mut menu_consts = quote! {};
let mut hotbar_slots_start = 0;
let mut hotbar_slots_end = 0;
let mut inventory_without_hotbar_slots_start = 0;
let mut inventory_without_hotbar_slots_end = 0;
for menu in &input.menus {
slot_mut_match_variants.extend(generate_match_variant_for_slot_mut(menu, true));
slot_match_variants.extend(generate_match_variant_for_slot_mut(menu, false));
len_match_variants.extend(generate_match_variant_for_len(menu));
kind_match_variants.extend(generate_match_variant_for_kind(menu));
slots_match_variants.extend(generate_match_variant_for_slots(menu));
contents_match_variants.extend(generate_match_variant_for_contents(menu));
location_match_variants.extend(generate_match_variant_for_location(menu));
player_slots_range_match_variants
.extend(generate_match_variant_for_player_slots_range(menu));
// this part is only used to generate `Player::is_hotbar_slot`
if menu.name == "Player" {
let mut i = 0;
for field in &menu.fields {
let field_name = &field.name;
let start = i;
i += field.length;
let end = i - 1;
if field_name == "inventory" {
// it only subtracts 8 here since it's inclusive (there's 9 total hotbar slots)
hotbar_slots_start = end - 8;
hotbar_slots_end = end;
inventory_without_hotbar_slots_start = start;
inventory_without_hotbar_slots_end = end - 9;
}
if start == end {
let const_name = Ident::new(
&format!("{}_SLOT", field_name.to_string().to_uppercase()),
field_name.span(),
);
player_consts.extend(quote! {
pub const #const_name: usize = #start;
});
} else {
let const_name = Ident::new(
&format!("{}_SLOTS", field_name.to_string().to_uppercase()),
field_name.span(),
);
player_consts.extend(quote! {
pub const #const_name: RangeInclusive<usize> = #start..=#end;
});
}
}
} else {
menu_consts.extend(generate_menu_consts(menu));
}
}
assert!(hotbar_slots_start != 0 && hotbar_slots_end != 0);
quote! {
impl Player {
pub const HOTBAR_SLOTS: RangeInclusive<usize> = #hotbar_slots_start..=#hotbar_slots_end;
pub const INVENTORY_WITHOUT_HOTBAR_SLOTS: RangeInclusive<usize> = #inventory_without_hotbar_slots_start..=#inventory_without_hotbar_slots_end;
#player_consts
/// Returns whether the given protocol index is in the player's hotbar.
///
/// Equivalent to `Player::HOTBAR_SLOTS.contains(&i)`.
pub fn is_hotbar_slot(i: usize) -> bool {
Self::HOTBAR_SLOTS.contains(&i)
}
}
impl Menu {
#menu_consts
/// Get a mutable reference to the [`ItemSlot`] at the given protocol index.
///
/// If you're trying to get an item in a menu without caring about
/// protocol indexes, you should just `match` it and index the
/// [`ItemSlot`] you get.
///
/// Use [`Menu::slot`] if you don't need a mutable reference to the slot.
///
/// # Errors
///
/// Returns `None` if the index is out of bounds.
#[inline]
pub fn slot_mut(&mut self, i: usize) -> Option<&mut ItemSlot> {
Some(match self {
#slot_mut_match_variants
})
}
/// Get a reference to the [`ItemSlot`] at the given protocol index.
///
/// If you're trying to get an item in a menu without caring about
/// protocol indexes, you should just `match` it and index the
/// [`ItemSlot`] you get.
///
/// Use [`Menu::slot_mut`] if you need a mutable reference to the slot.
///
/// # Errors
///
/// Returns `None` if the index is out of bounds.
pub fn slot(&self, i: usize) -> Option<&ItemSlot> {
Some(match self {
#slot_match_variants
})
}
/// Returns the number of slots in the menu.
#[allow(clippy::len_without_is_empty)]
pub const fn len(&self) -> usize {
match self {
#len_match_variants
}
}
pub fn from_kind(kind: azalea_registry::MenuKind) -> Self {
match kind {
#kind_match_variants
}
}
/// Return the contents of the menu, including the player's inventory.
///
/// The indexes in this will match up with [`Menu::slot_mut`].
///
/// If you don't want to include the player's inventory, use [`Menu::contents`] instead.
pub fn slots(&self) -> Vec<ItemSlot> {
match self {
#slots_match_variants
}
}
/// Return the contents of the menu, not including the player's inventory.
///
/// If you want to include the player's inventory, use [`Menu::slots`] instead.
pub fn contents(&self) -> Vec<ItemSlot> {
match self {
#contents_match_variants
}
}
pub fn location_for_slot(&self, i: usize) -> Option<MenuLocation> {
Some(match self {
#location_match_variants
})
}
/// Get the range of slot indexes that contain the player's inventory. This may be different for each menu.
pub fn player_slots_range(&self) -> RangeInclusive<usize> {
match self {
#player_slots_range_match_variants
}
}
/// Get the range of slot indexes that contain the player's hotbar. This may be different for each menu.
pub fn hotbar_slots_range(&self) -> RangeInclusive<usize> {
// hotbar is always last 9 slots in the player's inventory
((*self.player_slots_range().end() - 8)..=*self.player_slots_range().end())
}
/// Get the range of slot indexes that contain the player's inventory, not including the hotbar. This may be different for each menu.
pub fn player_slots_without_hotbar_range(&self) -> RangeInclusive<usize> {
(*self.player_slots_range().start()..=*self.player_slots_range().end() - 9)
}
/// Returns whether the given index would be in the player's hotbar.
///
/// Equivalent to `self.hotbar_slots_range().contains(&i)`.
pub fn is_hotbar_slot(&self, i: usize) -> bool {
self.hotbar_slots_range().contains(&i)
}
}
}
}
/// Menu::Player {
/// craft_result,
/// craft,
/// armor,
/// inventory,
/// offhand,
/// } => {
/// match i {
/// 0 => craft_result,
/// 1..=4 => craft,
/// 5..=8 => armor,
/// // ...
/// _ => return None,
/// }
/// } // ...
pub fn generate_match_variant_for_slot_mut(menu: &Menu, mutable: bool) -> TokenStream {
let mut match_arms = quote! {};
let mut i = 0;
for field in &menu.fields {
let field_name = &field.name;
let start = i;
i += field.length;
let end = i - 1;
match_arms.extend(if start == end {
quote! { #start => #field_name, }
} else if start == 0 {
if mutable {
quote! { #start..=#end => &mut #field_name[i], }
} else {
quote! { #start..=#end => &#field_name[i], }
}
} else if mutable {
quote! { #start..=#end => &mut #field_name[i - #start], }
} else {
quote! { #start..=#end => &#field_name[i - #start], }
});
}
generate_matcher(
menu,
&quote! {
match i {
#match_arms
_ => return None
}
},
true,
)
}
pub fn generate_match_variant_for_len(menu: &Menu) -> TokenStream {
let length = menu.fields.iter().map(|f| f.length).sum::<usize>();
generate_matcher(
menu,
&quote! {
#length
},
false,
)
}
pub fn generate_match_variant_for_kind(menu: &Menu) -> TokenStream {
// azalea_registry::MenuKind::Generic9x3 => Menu::Generic9x3 { contents:
// Default::default(), player: Default::default() },
let menu_name = &menu.name;
let menu_field_names = if menu.name == "Player" {
// player isn't in MenuKind
return quote! {};
} else {
let mut menu_field_names = quote! {};
for field in &menu.fields {
let field_name = &field.name;
menu_field_names.extend(quote! { #field_name: Default::default(), })
}
quote! { { #menu_field_names } }
};
quote! {
azalea_registry::MenuKind::#menu_name => Menu::#menu_name #menu_field_names,
}
}
pub fn generate_match_variant_for_slots(menu: &Menu) -> TokenStream {
let mut instructions = quote! {};
let mut length = 0;
for field in &menu.fields {
let field_name = &field.name;
instructions.extend(if field.length == 1 {
quote! { items.push(#field_name.clone()); }
} else {
quote! { items.extend(#field_name.iter().cloned()); }
});
length += field.length;
}
generate_matcher(
menu,
&quote! {
let mut items = Vec::with_capacity(#length);
#instructions
items
},
true,
)
}
pub fn generate_match_variant_for_contents(menu: &Menu) -> TokenStream {
let mut instructions = quote! {};
let mut length = 0;
for field in &menu.fields {
let field_name = &field.name;
if field_name == "player" {
continue;
}
instructions.extend(if field.length == 1 {
quote! { items.push(#field_name.clone()); }
} else {
quote! { items.extend(#field_name.iter().cloned()); }
});
length += field.length;
}
generate_matcher(
menu,
&quote! {
let mut items = Vec::with_capacity(#length);
#instructions
items
},
true,
)
}
pub fn generate_match_variant_for_location(menu: &Menu) -> TokenStream {
let mut match_arms = quote! {};
let mut i = 0;
let menu_name = Ident::new(&to_pascal_case(&menu.name.to_string()), menu.name.span());
let menu_enum_name = Ident::new(&format!("{menu_name}MenuLocation"), menu_name.span());
for field in &menu.fields {
let field_name = Ident::new(&to_pascal_case(&field.name.to_string()), field.name.span());
let start = i;
i += field.length;
let end = i - 1;
match_arms.extend(if start == end {
quote! { #start => #menu_enum_name::#field_name, }
} else {
quote! { #start..=#end => #menu_enum_name::#field_name, }
});
}
generate_matcher(
menu,
&quote! {
MenuLocation::#menu_name(match i {
#match_arms
_ => return None
})
},
false,
)
}
pub fn generate_match_variant_for_player_slots_range(menu: &Menu) -> TokenStream {
// Menu::Player(Player { .. }) => Player::INVENTORY_SLOTS_RANGE,,
// Menu::Generic9x3 { .. } => Menu::GENERIC9X3_SLOTS_RANGE,
// ..
match menu.name.to_string().as_str() {
"Player" => {
quote! {
Menu::Player(Player { .. }) => Player::INVENTORY_SLOTS,
}
}
_ => {
let menu_name = &menu.name;
let menu_slots_range_name = Ident::new(
&format!(
"{}_PLAYER_SLOTS",
to_snake_case(&menu.name.to_string()).to_uppercase()
),
menu.name.span(),
);
quote! {
Menu::#menu_name { .. } => Menu::#menu_slots_range_name,
}
}
}
}
fn generate_menu_consts(menu: &Menu) -> TokenStream {
let mut menu_consts = quote! {};
let mut i = 0;
for field in &menu.fields {
let field_name_start = format!(
"{}_{}",
to_snake_case(&menu.name.to_string()).to_uppercase(),
to_snake_case(&field.name.to_string()).to_uppercase()
);
let field_index_start = i;
i += field.length;
let field_index_end = i - 1;
if field.length == 1 {
let field_name = Ident::new(
format!("{}_SLOT", field_name_start).as_str(),
field.name.span(),
);
menu_consts.extend(quote! { pub const #field_name: usize = #field_index_start; });
} else {
let field_name = Ident::new(
format!("{}_SLOTS", field_name_start).as_str(),
field.name.span(),
);
menu_consts.extend(quote! { pub const #field_name: RangeInclusive<usize> = #field_index_start..=#field_index_end; });
}
}
menu_consts
}
pub fn generate_matcher(menu: &Menu, match_arms: &TokenStream, needs_fields: bool) -> TokenStream {
let menu_name = &menu.name;
let menu_field_names = if needs_fields {
let mut menu_field_names = quote! {};
for field in &menu.fields {
let field_name = &field.name;
menu_field_names.extend(quote! { #field_name, })
}
menu_field_names
} else {
quote! { .. }
};
let matcher = if menu.name == "Player" {
quote! { (Player { #menu_field_names }) }
} else {
quote! { { #menu_field_names } }
};
quote! {
Menu::#menu_name #matcher => {
#match_arms
},
}
}

View file

@ -0,0 +1,69 @@
use syn::{
self, braced,
parse::{Parse, ParseStream, Result},
Ident, LitInt, Token,
};
/// An identifier, colon, and number
/// `craft_result: 1`
pub struct Field {
pub name: Ident,
pub length: usize,
}
impl Parse for Field {
fn parse(input: ParseStream) -> Result<Self> {
let name = input.parse::<Ident>()?;
let _ = input.parse::<Token![:]>()?;
let length = input.parse::<LitInt>()?.base10_parse()?;
Ok(Self { name, length })
}
}
/// An identifier and a list of `Field` in curly brackets
/// ```rust,ignore
/// Player {
/// craft_result: 1,
/// ...
/// }
/// ```
pub struct Menu {
/// The menu name, e.g. `Player`
pub name: Ident,
pub fields: Vec<Field>,
}
impl Parse for Menu {
fn parse(input: ParseStream) -> Result<Self> {
let name = input.parse::<Ident>()?;
let content;
braced!(content in input);
let fields = content
.parse_terminated::<Field, Token![,]>(Field::parse)?
.into_iter()
.collect();
Ok(Self { name, fields })
}
}
/// A list of `Menu`s
/// ```rust,ignore
/// Player {
/// craft_result: 1,
/// ...
/// },
/// ...
/// ```
pub struct DeclareMenus {
pub menus: Vec<Menu>,
}
impl Parse for DeclareMenus {
fn parse(input: ParseStream) -> Result<Self> {
let menus = input
.parse_terminated::<Menu, Token![,]>(Menu::parse)?
.into_iter()
.collect();
Ok(Self { menus })
}
}

View file

@ -0,0 +1,54 @@
pub fn to_pascal_case(s: &str) -> String {
// we get the first item later so this is to make it impossible for that
// to error
if s.is_empty() {
return String::new();
}
let mut result = String::new();
let mut prev_was_underscore = true; // set to true by default so the first character is capitalized
if s.chars().next().unwrap().is_numeric() {
result.push('_');
}
for c in s.chars() {
if c == '_' {
prev_was_underscore = true;
} else if prev_was_underscore {
result.push(c.to_ascii_uppercase());
prev_was_underscore = false;
} else {
result.push(c);
}
}
result
}
pub fn to_snake_case(s: &str) -> String {
let mut result = String::new();
let mut prev_was_uppercase = true;
for c in s.chars() {
if c.is_ascii_uppercase() {
if !prev_was_uppercase {
result.push('_');
}
result.push(c.to_ascii_lowercase());
prev_was_uppercase = true;
} else {
result.push(c);
prev_was_uppercase = false;
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_snake_case() {
assert_eq!(to_snake_case("HelloWorld"), "hello_world");
assert_eq!(to_snake_case("helloWorld"), "hello_world");
assert_eq!(to_snake_case("hello_world"), "hello_world");
}
}

View file

@ -0,0 +1,21 @@
pub trait MaxStackSizeExt {
/// Get the maximum stack size for this item.
///
/// This is a signed integer to be consistent with the `count` field of
/// [`ItemSlotData`].
fn max_stack_size(&self) -> i8;
/// Whether this item can be stacked with other items.
///
/// This is equivalent to `self.max_stack_size() > 1`.
fn stackable(&self) -> bool {
self.max_stack_size() > 1
}
}
impl MaxStackSizeExt for azalea_registry::Item {
fn max_stack_size(&self) -> i8 {
// TODO: have the properties for every item defined somewhere
64
}
}

172
azalea-inventory/src/lib.rs Normal file
View file

@ -0,0 +1,172 @@
#![doc = include_str!("../README.md")]
pub mod item;
pub mod operations;
mod slot;
use std::ops::{Deref, DerefMut, RangeInclusive};
use azalea_inventory_macros::declare_menus;
pub use slot::{ItemSlot, ItemSlotData};
// TODO: remove this here and in azalea-inventory-macros when rust makes
// Default be implemented for all array sizes (since right now it's only up to
// 32)
/// A fixed-size list of [`ItemSlot`]s.
#[derive(Debug, Clone)]
pub struct SlotList<const N: usize>([ItemSlot; N]);
impl<const N: usize> Deref for SlotList<N> {
type Target = [ItemSlot; N];
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<const N: usize> DerefMut for SlotList<N> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl<const N: usize> Default for SlotList<N> {
fn default() -> Self {
SlotList([(); N].map(|_| ItemSlot::Empty))
}
}
impl Menu {
/// Get the [`Player`] from this [`Menu`].
///
/// # Panics
///
/// Will panic if the menu isn't `Menu::Player`.
pub fn as_player(&self) -> &Player {
if let Menu::Player(player) = &self {
player
} else {
unreachable!("Called `Menu::as_player` on a menu that wasn't `Player`.")
}
}
}
// the player inventory part is always the last 36 slots (except in the Player
// menu), so we don't have to explicitly specify it
// Client {
// ...
// pub menu: Menu,
// pub inventory: Arc<[Slot; 36]>
// }
// Generate a `struct Player`, `enum Menu`, and `impl Menu`.
// a "player" field gets implicitly added with the player inventory
declare_menus! {
Player {
craft_result: 1,
craft: 4,
armor: 4,
inventory: 36,
offhand: 1,
},
Generic9x1 {
contents: 9,
},
Generic9x2 {
contents: 18,
},
Generic9x3 {
contents: 27,
},
Generic9x4 {
contents: 36,
},
Generic9x5 {
contents: 45,
},
Generic9x6 {
contents: 54,
},
Generic3x3 {
contents: 9,
},
Anvil {
first: 1,
second: 1,
result: 1,
},
Beacon {
payment: 1,
},
BlastFurnace {
ingredient: 1,
fuel: 1,
result: 1,
},
BrewingStand {
bottles: 3,
ingredient: 1,
fuel: 1,
},
Crafting {
result: 1,
grid: 9,
},
Enchantment {
item: 1,
lapis: 1,
},
Furnace {
ingredient: 1,
fuel: 1,
result: 1,
},
Grindstone {
input: 1,
additional: 1,
result: 1,
},
Hopper {
contents: 5,
},
Lectern {
book: 1,
},
Loom {
banner: 1,
dye: 1,
pattern: 1,
result: 1,
},
Merchant {
payments: 2,
result: 1,
},
ShulkerBox {
contents: 27,
},
LegacySmithing {
input: 1,
additional: 1,
result: 1,
},
Smithing {
template: 1,
base: 1,
additional: 1,
result: 1,
},
Smoker {
ingredient: 1,
fuel: 1,
result: 1,
},
CartographyTable {
map: 1,
additional: 1,
result: 1,
},
Stonecutter {
input: 1,
result: 1,
},
}

View file

@ -0,0 +1,698 @@
use std::ops::RangeInclusive;
use azalea_buf::McBuf;
use crate::{
item::MaxStackSizeExt, AnvilMenuLocation, BeaconMenuLocation, BlastFurnaceMenuLocation,
BrewingStandMenuLocation, CartographyTableMenuLocation, CraftingMenuLocation,
EnchantmentMenuLocation, FurnaceMenuLocation, Generic3x3MenuLocation, Generic9x1MenuLocation,
Generic9x2MenuLocation, Generic9x3MenuLocation, Generic9x4MenuLocation, Generic9x5MenuLocation,
Generic9x6MenuLocation, GrindstoneMenuLocation, HopperMenuLocation, ItemSlot, ItemSlotData,
LecternMenuLocation, LegacySmithingMenuLocation, LoomMenuLocation, Menu, MenuLocation,
MerchantMenuLocation, Player, PlayerMenuLocation, ShulkerBoxMenuLocation, SmithingMenuLocation,
SmokerMenuLocation, StonecutterMenuLocation,
};
#[derive(Debug, Clone)]
pub enum ClickOperation {
Pickup(PickupClick),
QuickMove(QuickMoveClick),
Swap(SwapClick),
Clone(CloneClick),
Throw(ThrowClick),
QuickCraft(QuickCraftClick),
PickupAll(PickupAllClick),
}
#[derive(Debug, Clone)]
pub enum PickupClick {
/// Left mouse click. Note that in the protocol, None is represented as
/// -999.
Left { slot: Option<u16> },
/// Right mouse click. Note that in the protocol, None is represented as
/// -999.
Right { slot: Option<u16> },
/// Drop cursor stack.
LeftOutside,
/// Drop cursor single item.
RightOutside,
}
impl From<PickupClick> for ClickOperation {
fn from(click: PickupClick) -> Self {
ClickOperation::Pickup(click)
}
}
/// Shift click
#[derive(Debug, Clone)]
pub enum QuickMoveClick {
/// Shift + left mouse click
Left { slot: u16 },
/// Shift + right mouse click (identical behavior)
Right { slot: u16 },
}
impl From<QuickMoveClick> for ClickOperation {
fn from(click: QuickMoveClick) -> Self {
ClickOperation::QuickMove(click)
}
}
/// Used when you press number keys or F in an inventory.
#[derive(Debug, Clone)]
pub struct SwapClick {
pub source_slot: u16,
pub target_slot: u8,
}
impl From<SwapClick> for ClickOperation {
fn from(click: SwapClick) -> Self {
ClickOperation::Swap(click)
}
}
/// Middle click, only defined for creative players in non-player
/// inventories.
#[derive(Debug, Clone)]
pub struct CloneClick {
pub slot: u16,
}
impl From<CloneClick> for ClickOperation {
fn from(click: CloneClick) -> Self {
ClickOperation::Clone(click)
}
}
#[derive(Debug, Clone)]
pub enum ThrowClick {
/// Drop key (Q)
Single { slot: u16 },
/// Ctrl + drop key (Q)
All { slot: u16 },
}
impl From<ThrowClick> for ClickOperation {
fn from(click: ThrowClick) -> Self {
ClickOperation::Throw(click)
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct QuickCraftClick {
pub kind: QuickCraftKind,
pub status: QuickCraftStatus,
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum QuickCraftKind {
Left,
Right,
Middle,
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum QuickCraftStatusKind {
/// Starting drag
Start,
/// Add slot
Add,
/// Ending drag
End,
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum QuickCraftStatus {
/// Starting drag
Start,
/// Add a slot.
Add { slot: u16 },
/// Ending drag
End,
}
impl From<QuickCraftStatus> for QuickCraftStatusKind {
fn from(status: QuickCraftStatus) -> Self {
match status {
QuickCraftStatus::Start => QuickCraftStatusKind::Start,
QuickCraftStatus::Add { .. } => QuickCraftStatusKind::Add,
QuickCraftStatus::End => QuickCraftStatusKind::End,
}
}
}
/// Double click
#[derive(Debug, Clone)]
pub struct PickupAllClick {
/// The slot that we're double clicking on. It should be empty or at least
/// not pickup-able (since the carried item is used as the filter).
pub slot: u16,
/// Impossible in vanilla clients.
pub reversed: bool,
}
impl From<PickupAllClick> for ClickOperation {
fn from(click: PickupAllClick) -> Self {
ClickOperation::PickupAll(click)
}
}
impl ClickOperation {
/// Return the slot number that this operation is acting on, if any.
///
/// Note that in the protocol, "None" is represented as -999.
pub fn slot_num(&self) -> Option<u16> {
match self {
ClickOperation::Pickup(pickup) => match pickup {
PickupClick::Left { slot } => *slot,
PickupClick::Right { slot } => *slot,
PickupClick::LeftOutside => None,
PickupClick::RightOutside => None,
},
ClickOperation::QuickMove(quick_move) => match quick_move {
QuickMoveClick::Left { slot } => Some(*slot),
QuickMoveClick::Right { slot } => Some(*slot),
},
ClickOperation::Swap(swap) => Some(swap.source_slot),
ClickOperation::Clone(clone) => Some(clone.slot),
ClickOperation::Throw(throw) => match throw {
ThrowClick::Single { slot } => Some(*slot),
ThrowClick::All { slot } => Some(*slot),
},
ClickOperation::QuickCraft(quick_craft) => match quick_craft.status {
QuickCraftStatus::Start => None,
QuickCraftStatus::Add { slot } => Some(slot),
QuickCraftStatus::End => None,
},
ClickOperation::PickupAll(pickup_all) => Some(pickup_all.slot),
}
}
pub fn button_num(&self) -> u8 {
match self {
ClickOperation::Pickup(pickup) => match pickup {
PickupClick::Left { .. } => 0,
PickupClick::Right { .. } => 1,
PickupClick::LeftOutside => 0,
PickupClick::RightOutside => 1,
},
ClickOperation::QuickMove(quick_move) => match quick_move {
QuickMoveClick::Left { .. } => 0,
QuickMoveClick::Right { .. } => 1,
},
ClickOperation::Swap(swap) => swap.target_slot,
ClickOperation::Clone(_) => 2,
ClickOperation::Throw(throw) => match throw {
ThrowClick::Single { .. } => 0,
ThrowClick::All { .. } => 1,
},
ClickOperation::QuickCraft(quick_craft) => match quick_craft {
QuickCraftClick {
kind: QuickCraftKind::Left,
status: QuickCraftStatus::Start,
} => 0,
QuickCraftClick {
kind: QuickCraftKind::Right,
status: QuickCraftStatus::Start,
} => 4,
QuickCraftClick {
kind: QuickCraftKind::Middle,
status: QuickCraftStatus::Start,
} => 8,
QuickCraftClick {
kind: QuickCraftKind::Left,
status: QuickCraftStatus::Add { .. },
} => 1,
QuickCraftClick {
kind: QuickCraftKind::Right,
status: QuickCraftStatus::Add { .. },
} => 5,
QuickCraftClick {
kind: QuickCraftKind::Middle,
status: QuickCraftStatus::Add { .. },
} => 9,
QuickCraftClick {
kind: QuickCraftKind::Left,
status: QuickCraftStatus::End,
} => 2,
QuickCraftClick {
kind: QuickCraftKind::Right,
status: QuickCraftStatus::End,
} => 6,
QuickCraftClick {
kind: QuickCraftKind::Middle,
status: QuickCraftStatus::End,
} => 10,
},
ClickOperation::PickupAll(_) => 0,
}
}
pub fn click_type(&self) -> ClickType {
match self {
ClickOperation::Pickup(_) => ClickType::Pickup,
ClickOperation::QuickMove(_) => ClickType::QuickMove,
ClickOperation::Swap(_) => ClickType::Swap,
ClickOperation::Clone(_) => ClickType::Clone,
ClickOperation::Throw(_) => ClickType::Throw,
ClickOperation::QuickCraft(_) => ClickType::QuickCraft,
ClickOperation::PickupAll(_) => ClickType::PickupAll,
}
}
}
#[derive(McBuf, Clone, Copy, Debug)]
pub enum ClickType {
Pickup = 0,
QuickMove = 1,
Swap = 2,
Clone = 3,
Throw = 4,
QuickCraft = 5,
PickupAll = 6,
}
impl Menu {
/// Shift-click a slot in this menu.
pub fn quick_move_stack(&mut self, slot_index: usize) -> ItemSlot {
let slot = self.slot(slot_index);
if slot.is_none() {
return ItemSlot::Empty;
};
let slot_location = self
.location_for_slot(slot_index)
.expect("we just checked to make sure the slot is Some above, so this shouldn't be able to error");
match slot_location {
MenuLocation::Player(l) => match l {
PlayerMenuLocation::CraftResult => {
self.try_move_item_to_slots(slot_index, Player::INVENTORY_SLOTS);
}
PlayerMenuLocation::Craft => {
self.try_move_item_to_slots(slot_index, Player::INVENTORY_SLOTS);
}
PlayerMenuLocation::Armor => {
self.try_move_item_to_slots(slot_index, Player::INVENTORY_SLOTS);
}
_ => {
// TODO: armor handling (see quickMoveStack in
// InventoryMenu.java)
// if slot.kind().is_armor() &&
// also offhand handling
if l == PlayerMenuLocation::Inventory {
// shift-clicking in hotbar moves to inventory, and vice versa
if Player::is_hotbar_slot(slot_index) {
self.try_move_item_to_slots(
slot_index,
Player::INVENTORY_WITHOUT_HOTBAR_SLOTS,
);
} else {
self.try_move_item_to_slots(slot_index, Player::HOTBAR_SLOTS);
}
} else {
self.try_move_item_to_slots(slot_index, self.player_slots_range());
}
}
},
MenuLocation::Generic9x1(l) => match l {
Generic9x1MenuLocation::Contents => {
self.try_move_item_to_slots(slot_index, self.player_slots_range());
}
Generic9x1MenuLocation::Player => {
self.try_move_item_to_slots_or_toggle_hotbar(
slot_index,
Menu::GENERIC9X1_CONTENTS_SLOTS,
);
}
},
MenuLocation::Generic9x2(l) => match l {
Generic9x2MenuLocation::Contents => {
self.try_move_item_to_slots(slot_index, self.player_slots_range());
}
Generic9x2MenuLocation::Player => {
self.try_move_item_to_slots_or_toggle_hotbar(
slot_index,
Menu::GENERIC9X2_CONTENTS_SLOTS,
);
}
},
MenuLocation::Generic9x3(l) => match l {
Generic9x3MenuLocation::Contents => {
self.try_move_item_to_slots(slot_index, self.player_slots_range());
}
Generic9x3MenuLocation::Player => {
self.try_move_item_to_slots_or_toggle_hotbar(
slot_index,
Menu::GENERIC9X3_CONTENTS_SLOTS,
);
}
},
MenuLocation::Generic9x4(l) => match l {
Generic9x4MenuLocation::Contents => {
self.try_move_item_to_slots(slot_index, self.player_slots_range());
}
Generic9x4MenuLocation::Player => {
self.try_move_item_to_slots_or_toggle_hotbar(
slot_index,
Menu::GENERIC9X4_CONTENTS_SLOTS,
);
}
},
MenuLocation::Generic9x5(l) => match l {
Generic9x5MenuLocation::Contents => {
self.try_move_item_to_slots(slot_index, self.player_slots_range());
}
Generic9x5MenuLocation::Player => {
self.try_move_item_to_slots_or_toggle_hotbar(
slot_index,
Menu::GENERIC9X5_CONTENTS_SLOTS,
);
}
},
MenuLocation::Generic9x6(l) => match l {
Generic9x6MenuLocation::Contents => {
self.try_move_item_to_slots(slot_index, self.player_slots_range());
}
Generic9x6MenuLocation::Player => {
self.try_move_item_to_slots_or_toggle_hotbar(
slot_index,
Menu::GENERIC9X6_CONTENTS_SLOTS,
);
}
},
MenuLocation::Generic3x3(l) => match l {
Generic3x3MenuLocation::Contents => {
self.try_move_item_to_slots(slot_index, self.player_slots_range());
}
Generic3x3MenuLocation::Player => {
self.try_move_item_to_slots_or_toggle_hotbar(
slot_index,
Menu::GENERIC3X3_CONTENTS_SLOTS,
);
}
},
MenuLocation::Anvil(l) => match l {
AnvilMenuLocation::Player => {
self.try_move_item_to_slots_or_toggle_hotbar(
slot_index,
Menu::ANVIL_FIRST_SLOT..=Menu::ANVIL_SECOND_SLOT,
);
}
_ => {
self.try_move_item_to_slots(slot_index, self.player_slots_range());
}
},
MenuLocation::Beacon(l) => match l {
BeaconMenuLocation::Payment => {
self.try_move_item_to_slots(slot_index, self.player_slots_range());
}
BeaconMenuLocation::Player => {
self.try_move_item_to_slots(
slot_index,
Menu::BEACON_PAYMENT_SLOT..=Menu::BEACON_PAYMENT_SLOT,
);
}
},
MenuLocation::BlastFurnace(l) => match l {
BlastFurnaceMenuLocation::Player => {
self.try_move_item_to_slots(
slot_index,
Menu::BLAST_FURNACE_INGREDIENT_SLOT..=Menu::BLAST_FURNACE_FUEL_SLOT,
);
}
_ => {
self.try_move_item_to_slots(slot_index, self.player_slots_range());
}
},
MenuLocation::BrewingStand(l) => match l {
BrewingStandMenuLocation::Player => {
self.try_move_item_to_slots(
slot_index,
*Menu::BREWING_STAND_BOTTLES_SLOTS.start()
..=Menu::BREWING_STAND_INGREDIENT_SLOT,
);
}
_ => {
self.try_move_item_to_slots(slot_index, self.player_slots_range());
}
},
MenuLocation::Crafting(l) => match l {
CraftingMenuLocation::Player => {
self.try_move_item_to_slots_or_toggle_hotbar(
slot_index,
Menu::CRAFTING_GRID_SLOTS,
);
}
_ => {
self.try_move_item_to_slots(slot_index, self.player_slots_range());
}
},
MenuLocation::Enchantment(l) => match l {
EnchantmentMenuLocation::Player => {
self.try_move_item_to_slots_or_toggle_hotbar(
slot_index,
Menu::ENCHANTMENT_ITEM_SLOT..=Menu::ENCHANTMENT_LAPIS_SLOT,
);
}
_ => {
self.try_move_item_to_slots(slot_index, self.player_slots_range());
}
},
MenuLocation::Furnace(l) => match l {
FurnaceMenuLocation::Player => {
self.try_move_item_to_slots(
slot_index,
Menu::FURNACE_INGREDIENT_SLOT..=Menu::FURNACE_FUEL_SLOT,
);
}
_ => {
self.try_move_item_to_slots(slot_index, self.player_slots_range());
}
},
MenuLocation::Grindstone(l) => match l {
GrindstoneMenuLocation::Player => {
self.try_move_item_to_slots_or_toggle_hotbar(
slot_index,
Menu::GRINDSTONE_INPUT_SLOT..=Menu::GRINDSTONE_ADDITIONAL_SLOT,
);
}
_ => {
self.try_move_item_to_slots(slot_index, self.player_slots_range());
}
},
MenuLocation::Hopper(l) => match l {
HopperMenuLocation::Player => {
self.try_move_item_to_slots_or_toggle_hotbar(
slot_index,
Menu::HOPPER_CONTENTS_SLOTS,
);
}
_ => {
self.try_move_item_to_slots(slot_index, self.player_slots_range());
}
},
MenuLocation::Lectern(l) => match l {
LecternMenuLocation::Player => {
self.try_move_item_to_slots_or_toggle_hotbar(
slot_index,
Menu::LECTERN_BOOK_SLOT..=Menu::LECTERN_BOOK_SLOT,
);
}
_ => {
self.try_move_item_to_slots(slot_index, self.player_slots_range());
}
},
MenuLocation::Loom(l) => match l {
LoomMenuLocation::Player => {
self.try_move_item_to_slots_or_toggle_hotbar(
slot_index,
Menu::LOOM_BANNER_SLOT..=Menu::LOOM_PATTERN_SLOT,
);
}
_ => {
self.try_move_item_to_slots(slot_index, self.player_slots_range());
}
},
MenuLocation::Merchant(l) => match l {
MerchantMenuLocation::Player => {
self.try_move_item_to_slots_or_toggle_hotbar(
slot_index,
Menu::MERCHANT_PAYMENTS_SLOTS,
);
}
_ => {
self.try_move_item_to_slots(slot_index, self.player_slots_range());
}
},
MenuLocation::ShulkerBox(l) => match l {
ShulkerBoxMenuLocation::Player => {
self.try_move_item_to_slots_or_toggle_hotbar(
slot_index,
Menu::SHULKER_BOX_CONTENTS_SLOTS,
);
}
_ => {
self.try_move_item_to_slots(slot_index, self.player_slots_range());
}
},
MenuLocation::LegacySmithing(l) => match l {
LegacySmithingMenuLocation::Player => {
self.try_move_item_to_slots_or_toggle_hotbar(
slot_index,
Menu::LEGACY_SMITHING_INPUT_SLOT..=Menu::LEGACY_SMITHING_ADDITIONAL_SLOT,
);
}
_ => {
self.try_move_item_to_slots(slot_index, self.player_slots_range());
}
},
MenuLocation::Smithing(l) => match l {
SmithingMenuLocation::Player => {
self.try_move_item_to_slots_or_toggle_hotbar(
slot_index,
Menu::SMITHING_TEMPLATE_SLOT..=Menu::SMITHING_ADDITIONAL_SLOT,
);
}
_ => {
self.try_move_item_to_slots(slot_index, self.player_slots_range());
}
},
MenuLocation::Smoker(l) => match l {
SmokerMenuLocation::Player => {
self.try_move_item_to_slots(
slot_index,
Menu::SMOKER_INGREDIENT_SLOT..=Menu::SMOKER_FUEL_SLOT,
);
}
_ => {
self.try_move_item_to_slots(slot_index, self.player_slots_range());
}
},
MenuLocation::CartographyTable(l) => match l {
CartographyTableMenuLocation::Player => {
self.try_move_item_to_slots_or_toggle_hotbar(
slot_index,
Menu::CARTOGRAPHY_TABLE_MAP_SLOT..=Menu::CARTOGRAPHY_TABLE_ADDITIONAL_SLOT,
);
}
_ => {
self.try_move_item_to_slots(slot_index, self.player_slots_range());
}
},
MenuLocation::Stonecutter(l) => match l {
StonecutterMenuLocation::Player => {
self.try_move_item_to_slots_or_toggle_hotbar(
slot_index,
Menu::STONECUTTER_INPUT_SLOT..=Menu::STONECUTTER_INPUT_SLOT,
);
}
_ => {
self.try_move_item_to_slots(slot_index, self.player_slots_range());
}
},
}
ItemSlot::Empty
}
fn try_move_item_to_slots_or_toggle_hotbar(
&mut self,
slot_index: usize,
target_slot_indexes: RangeInclusive<usize>,
) {
if !self.try_move_item_to_slots(slot_index, target_slot_indexes) {
self.try_move_item_to_slots(
slot_index,
if self.is_hotbar_slot(slot_index) {
self.player_slots_without_hotbar_range()
} else {
self.hotbar_slots_range()
},
);
}
}
/// Whether the given item could be placed in this menu.
///
/// TODO: right now this always returns true
pub fn may_place(&self, _target_slot_index: usize, _item: &ItemSlotData) -> bool {
true
}
/// Whether the item in the given slot could be clicked and picked up.
/// TODO: right now this always returns true
pub fn may_pickup(&self, _source_slot_index: usize) -> bool {
true
}
/// Get the maximum number of items that can be placed in this slot.
pub fn max_stack_size(&self, _target_slot_index: usize) -> u8 {
64
}
/// Try moving an item to a set of slots in this menu.
///
/// Returns the updated item slot.
fn try_move_item_to_slots(
&mut self,
item_slot_index: usize,
target_slot_indexes: RangeInclusive<usize>,
) -> bool {
let mut item_slot = self.slot(item_slot_index).unwrap().clone();
// first see if we can stack it with another item
if item_slot.kind().stackable() {
for target_slot_index in target_slot_indexes.clone() {
self.move_item_to_slot_if_stackable(&mut item_slot, target_slot_index);
if item_slot.is_empty() {
break;
}
}
}
// and if not then just try putting it in an empty slot
if item_slot.is_present() {
for target_slot_index in target_slot_indexes {
self.move_item_to_slot_if_empty(&mut item_slot, target_slot_index);
if item_slot.is_empty() {
break;
}
}
}
item_slot.is_empty()
}
/// Merge this item slot into the target item slot, only if the target item
/// slot is present and the same item.
fn move_item_to_slot_if_stackable(
&mut self,
item_slot: &mut ItemSlot,
target_slot_index: usize,
) {
let ItemSlot::Present(item) = item_slot else {
return;
};
let target_slot = self.slot(target_slot_index).unwrap();
if let ItemSlot::Present(target_item) = target_slot {
// the target slot is empty, so we can just move the item there
if self.may_place(target_slot_index, item) && target_item.is_same_item_and_nbt(item) {
let slot_item_limit = self.max_stack_size(target_slot_index);
let new_target_slot_data = item.split(u8::min(slot_item_limit, item.count as u8));
// get the target slot again but mut this time so we can update it
let target_slot = self.slot_mut(target_slot_index).unwrap();
*target_slot = ItemSlot::Present(new_target_slot_data);
item_slot.update_empty();
}
}
}
fn move_item_to_slot_if_empty(&mut self, item_slot: &mut ItemSlot, target_slot_index: usize) {
let ItemSlot::Present(item) = item_slot else {
return;
};
let target_slot = self.slot(target_slot_index).unwrap();
if target_slot.is_empty() && self.may_place(target_slot_index, item) {
let slot_item_limit = self.max_stack_size(target_slot_index);
let new_target_slot_data = item.split(u8::min(slot_item_limit, item.count as u8));
let target_slot = self.slot_mut(target_slot_index).unwrap();
*target_slot = ItemSlot::Present(new_target_slot_data);
item_slot.update_empty();
}
}
}

View file

@ -0,0 +1,146 @@
use azalea_buf::{BufReadError, McBuf, McBufReadable, McBufWritable};
use azalea_nbt::Nbt;
use std::io::{Cursor, Write};
/// Either an item in an inventory or nothing.
#[derive(Debug, Clone, Default, PartialEq)]
pub enum ItemSlot {
#[default]
Empty,
Present(ItemSlotData),
}
impl ItemSlot {
/// Check if the slot is ItemSlot::Empty, if the count is <= 0, or if the
/// item is air.
///
/// This is the opposite of [`ItemSlot::is_present`].
pub fn is_empty(&self) -> bool {
match self {
ItemSlot::Empty => true,
ItemSlot::Present(item) => item.is_empty(),
}
}
/// Check if the slot is not ItemSlot::Empty, if the count is > 0, and if
/// the item is not air.
///
/// This is the opposite of [`ItemSlot::is_empty`].
pub fn is_present(&self) -> bool {
!self.is_empty()
}
/// Return the amount of the item in the slot, or 0 if the slot is empty.
///
/// Note that it's possible for the count to be zero or negative when the
/// slot is present.
pub fn count(&self) -> i8 {
match self {
ItemSlot::Empty => 0,
ItemSlot::Present(i) => i.count,
}
}
/// Remove `count` items from this slot, returning the removed items.
pub fn split(&mut self, count: u8) -> ItemSlot {
match self {
ItemSlot::Empty => ItemSlot::Empty,
ItemSlot::Present(i) => {
let returning = i.split(count);
if i.is_empty() {
*self = ItemSlot::Empty;
}
ItemSlot::Present(returning)
}
}
}
/// Get the `kind` of the item in this slot, or
/// [`azalea_registry::Item::Air`]
pub fn kind(&self) -> azalea_registry::Item {
match self {
ItemSlot::Empty => azalea_registry::Item::Air,
ItemSlot::Present(i) => i.kind,
}
}
/// Update whether this slot is empty, based on the count.
pub fn update_empty(&mut self) {
if let ItemSlot::Present(i) = self {
if i.is_empty() {
*self = ItemSlot::Empty;
}
}
}
}
/// An item in an inventory, with a count and NBT. Usually you want [`ItemSlot`]
/// or [`azalea_registry::Item`] instead.
#[derive(Debug, Clone, McBuf, PartialEq)]
pub struct ItemSlotData {
pub kind: azalea_registry::Item,
/// The amount of the item in this slot.
///
/// The count can be zero or negative, but this is rare.
pub count: i8,
pub nbt: Nbt,
}
impl ItemSlotData {
/// Remove `count` items from this slot, returning the removed items.
pub fn split(&mut self, count: u8) -> ItemSlotData {
let returning_count = i8::min(count as i8, self.count);
let mut returning = self.clone();
returning.count = returning_count;
self.count -= returning_count;
returning
}
/// Check if the count of the item is <= 0 or if the item is air.
pub fn is_empty(&self) -> bool {
self.count <= 0 || self.kind == azalea_registry::Item::Air
}
/// Whether this item is the same as another item, ignoring the count.
///
/// ```
/// # use azalea_inventory::ItemSlotData;
/// # use azalea_registry::Item;
/// let mut a = ItemSlotData {
/// kind: Item::Stone,
/// count: 1,
/// nbt: Default::default(),
/// };
/// let mut b = ItemSlotData {
/// kind: Item::Stone,
/// count: 2,
/// nbt: Default::default(),
/// };
/// assert!(a.is_same_item_and_nbt(&b));
///
/// b.kind = Item::Dirt;
/// assert!(!a.is_same_item_and_nbt(&b));
/// ```
pub fn is_same_item_and_nbt(&self, other: &ItemSlotData) -> bool {
self.kind == other.kind && self.nbt == other.nbt
}
}
impl McBufReadable for ItemSlot {
fn read_from(buf: &mut Cursor<&[u8]>) -> Result<Self, BufReadError> {
let slot = Option::<ItemSlotData>::read_from(buf)?;
Ok(slot.map_or(ItemSlot::Empty, ItemSlot::Present))
}
}
impl McBufWritable for ItemSlot {
fn write_into(&self, buf: &mut impl Write) -> Result<(), std::io::Error> {
match self {
ItemSlot::Empty => false.write_into(buf)?,
ItemSlot::Present(i) => {
true.write_into(buf)?;
i.write_into(buf)?;
}
};
Ok(())
}
}

View file

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

View file

@ -11,6 +11,7 @@ version = "0.6.0"
[dependencies]
azalea-block = { path = "../azalea-block", version = "^0.6.0" }
azalea-core = { path = "../azalea-core", version = "^0.6.0" }
azalea-inventory = { version = "0.1.0", path = "../azalea-inventory" }
azalea-registry = { path = "../azalea-registry", version = "^0.6.0" }
azalea-world = { path = "../azalea-world", version = "^0.6.0" }
bevy_app = "0.10.0"

232
azalea-physics/src/clip.rs Normal file
View file

@ -0,0 +1,232 @@
use azalea_block::BlockState;
use azalea_core::{lerp, BlockHitResult, BlockPos, Direction, Vec3, EPSILON};
use azalea_inventory::ItemSlot;
use azalea_world::ChunkStorage;
use bevy_ecs::entity::Entity;
use crate::collision::{BlockWithShape, VoxelShape};
#[derive(Debug, Clone)]
pub struct ClipContext {
pub from: Vec3,
pub to: Vec3,
pub block_shape_type: BlockShapeType,
pub fluid_pick_type: FluidPickType,
// pub collision_context: EntityCollisionContext,
}
impl ClipContext {
// minecraft passes in the world and blockpos here... but it doesn't actually
// seem necessary?
pub fn block_shape(&self, block_state: BlockState) -> &VoxelShape {
// TODO: implement the other shape getters
// (see the ClipContext.Block class in the vanilla source)
match self.block_shape_type {
BlockShapeType::Collider => block_state.shape(),
BlockShapeType::Outline => block_state.shape(),
BlockShapeType::Visual => block_state.shape(),
BlockShapeType::FallDamageResetting => block_state.shape(),
}
}
}
#[derive(Debug, Copy, Clone)]
pub enum BlockShapeType {
Collider,
Outline,
Visual,
FallDamageResetting,
}
#[derive(Debug, Copy, Clone)]
pub enum FluidPickType {
None,
SourceOnly,
Any,
Water,
}
#[derive(Debug, Clone)]
pub struct EntityCollisionContext {
pub descending: bool,
pub entity_bottom: f64,
pub held_item: ItemSlot,
// pub can_stand_on_fluid: Box<dyn Fn(&FluidState) -> bool>,
pub entity: Entity,
}
pub fn clip(chunk_storage: &ChunkStorage, context: ClipContext) -> BlockHitResult {
traverse_blocks(
context.from,
context.to,
context,
|context, block_pos| {
let block_state = chunk_storage.get_block_state(block_pos).unwrap_or_default();
// TODO: add fluid stuff to this (see getFluidState in vanilla source)
let block_shape = context.block_shape(block_state);
clip_with_interaction_override(
&context.from,
&context.to,
block_pos,
block_shape,
&block_state,
)
// let block_distance = if let Some(block_hit_result) =
// block_hit_result { context.from.distance_to_sqr(&
// block_hit_result.location) } else {
// f64::MAX
// };
},
|context| {
let vec = context.from - context.to;
BlockHitResult::miss(
context.to,
Direction::nearest(vec),
BlockPos::from(context.to),
)
},
)
}
// default BlockHitResult clipWithInteractionOverride(Vec3 world, Vec3 from,
// BlockPos to, VoxelShape shape, BlockState block) {
// BlockHitResult blockHitResult = shape.clip(world, from, to);
// if (blockHitResult != null) {
// BlockHitResult var7 = block.getInteractionShape(this, to).clip(world,
// from, to); if (var7 != null
// && var7.getLocation().subtract(world).lengthSqr() <
// blockHitResult.getLocation().subtract(world).lengthSqr()) { return
// blockHitResult.withDirection(var7.getDirection()); }
// }
// return blockHitResult;
// }
fn clip_with_interaction_override(
from: &Vec3,
to: &Vec3,
block_pos: &BlockPos,
block_shape: &VoxelShape,
block_state: &BlockState,
) -> Option<BlockHitResult> {
let block_hit_result = block_shape.clip(from, to, block_pos);
if let Some(block_hit_result) = block_hit_result {
// TODO: minecraft calls .getInteractionShape here
// are there even any blocks that have a physics shape different from the
// interaction shape???
// (if not then you can delete this comment)
// (if there are then you have to implement BlockState::interaction_shape, lol
// have fun)
let interaction_shape = block_state.shape();
let interaction_hit_result = interaction_shape.clip(from, to, block_pos);
if let Some(interaction_hit_result) = interaction_hit_result {
if interaction_hit_result.location.distance_to_sqr(from)
< block_hit_result.location.distance_to_sqr(from)
{
return Some(block_hit_result.with_direction(interaction_hit_result.direction));
}
}
Some(block_hit_result)
} else {
block_hit_result
}
}
pub fn traverse_blocks<C, T>(
from: Vec3,
to: Vec3,
context: C,
get_hit_result: impl Fn(&C, &BlockPos) -> Option<T>,
get_miss_result: impl Fn(&C) -> T,
) -> T {
if from == to {
return get_miss_result(&context);
}
let right_after_end = Vec3 {
x: lerp(-EPSILON, to.x, from.x),
y: lerp(-EPSILON, to.y, from.y),
z: lerp(-EPSILON, to.z, from.z),
};
let right_before_start = Vec3 {
x: lerp(-EPSILON, from.x, to.x),
y: lerp(-EPSILON, from.y, to.y),
z: lerp(-EPSILON, from.z, to.z),
};
let mut current_block = BlockPos::from(right_before_start);
if let Some(data) = get_hit_result(&context, &current_block) {
return data;
}
let vec = right_after_end - right_before_start;
/// Returns either -1, 0, or 1, depending on whether the number is negative,
/// zero, or positive.
///
/// This function exists because f64::signum doesn't check for 0.
fn get_number_sign(num: f64) -> f64 {
if num == 0. {
0.
} else {
num.signum()
}
}
let vec_sign = Vec3 {
x: get_number_sign(vec.x),
y: get_number_sign(vec.y),
z: get_number_sign(vec.z),
};
#[rustfmt::skip]
let percentage_step = Vec3 {
x: if vec_sign.x == 0. { f64::MAX } else { vec_sign.x / vec.x },
y: if vec_sign.y == 0. { f64::MAX } else { vec_sign.y / vec.y },
z: if vec_sign.z == 0. { f64::MAX } else { vec_sign.z / vec.z },
};
let mut percentage = Vec3 {
x: percentage_step.x
* if vec_sign.x > 0. {
1. - right_before_start.x.fract()
} else {
right_before_start.x.fract().abs()
},
y: percentage_step.y
* if vec_sign.y > 0. {
1. - right_before_start.y.fract()
} else {
right_before_start.y.fract().abs()
},
z: percentage_step.z
* if vec_sign.z > 0. {
1. - right_before_start.z.fract()
} else {
right_before_start.z.fract().abs()
},
};
loop {
if percentage.x > 1. && percentage.y > 1. && percentage.z > 1. {
return get_miss_result(&context);
}
if percentage.x < percentage.y {
if percentage.x < percentage.z {
current_block.x += vec_sign.x as i32;
percentage.x += percentage_step.x;
} else {
current_block.z += vec_sign.z as i32;
percentage.z += percentage_step.z;
}
} else if percentage.y < percentage.z {
current_block.y += vec_sign.y as i32;
percentage.y += percentage_step.y;
} else {
current_block.z += vec_sign.z as i32;
percentage.z += percentage_step.z;
}
if let Some(data) = get_hit_result(&context, &current_block) {
return data;
}
}
}

View file

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

View file

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

View file

@ -1,9 +1,11 @@
use super::mergers::IndexMerger;
use crate::collision::{BitSetDiscreteVoxelShape, DiscreteVoxelShape, AABB};
use azalea_core::{binary_search, Axis, AxisCycle, EPSILON};
use azalea_core::{
binary_search, Axis, AxisCycle, BlockHitResult, BlockPos, Direction, Vec3, EPSILON,
};
use std::{cmp, num::NonZeroU32};
pub struct Shapes {}
pub struct Shapes;
pub fn block_shape() -> VoxelShape {
let mut shape = BitSetDiscreteVoxelShape::new(1, 1, 1);
@ -390,6 +392,33 @@ impl VoxelShape {
}
}
pub fn clip(&self, from: &Vec3, to: &Vec3, block_pos: &BlockPos) -> Option<BlockHitResult> {
if self.is_empty() {
return None;
}
let vector = to - from;
if vector.length_sqr() < EPSILON {
return None;
}
let right_after_start = from + &(vector * 0.0001);
if self.shape().is_full_wide(
self.find_index(Axis::X, right_after_start.x - block_pos.x as f64),
self.find_index(Axis::Y, right_after_start.y - block_pos.y as f64),
self.find_index(Axis::Z, right_after_start.z - block_pos.z as f64),
) {
Some(BlockHitResult {
block_pos: *block_pos,
direction: Direction::nearest(vector).opposite(),
location: right_after_start,
inside: true,
miss: false,
})
} else {
AABB::clip_iterable(&self.to_aabbs(), from, to, block_pos)
}
}
pub fn collide(&self, axis: &Axis, entity_box: &AABB, movement: f64) -> f64 {
self.collide_x(AxisCycle::between(*axis, Axis::X), entity_box, movement)
}
@ -531,19 +560,34 @@ impl VoxelShape {
let y_coords = self.get_coords(Axis::Y);
let z_coords = self.get_coords(Axis::Z);
self.shape().for_all_boxes(
|var4x, var5, var6, var7, var8, var9| {
|min_x, min_y, min_z, max_x, max_y, max_z| {
consumer(
x_coords[var4x as usize],
y_coords[var5 as usize],
z_coords[var6 as usize],
x_coords[var7 as usize],
y_coords[var8 as usize],
z_coords[var9 as usize],
x_coords[min_x as usize],
y_coords[min_y as usize],
z_coords[min_z as usize],
x_coords[max_x as usize],
y_coords[max_y as usize],
z_coords[max_z as usize],
);
},
true,
);
}
pub fn to_aabbs(&self) -> Vec<AABB> {
let mut aabbs = Vec::new();
self.for_all_boxes(|min_x, min_y, min_z, max_x, max_y, max_z| {
aabbs.push(AABB {
min_x,
min_y,
min_z,
max_x,
max_y,
max_z,
});
});
aabbs
}
}
impl From<AABB> for VoxelShape {

View file

@ -1,14 +1,15 @@
#![doc = include_str!("../README.md")]
#![feature(trait_alias)]
pub mod clip;
pub mod collision;
use azalea_block::{Block, BlockState};
use azalea_core::{BlockPos, Vec3};
use azalea_world::{
entity::{
metadata::Sprinting, move_relative, Attributes, Jumping, Local, Physics, Position,
WorldName,
clamp_look_direction, metadata::Sprinting, move_relative, Attributes, Jumping, Local,
LookDirection, Physics, Position, WorldName,
},
Instance, InstanceContainer,
};
@ -30,7 +31,11 @@ pub struct PhysicsPlugin;
impl Plugin for PhysicsPlugin {
fn build(&self, app: &mut App) {
app.add_event::<ForceJumpEvent>()
.add_system(force_jump_listener.before(azalea_world::entity::update_bounding_box))
.add_system(
force_jump_listener
.before(azalea_world::entity::update_bounding_box)
.after(clamp_look_direction),
)
.add_systems(
(ai_step, travel)
.chain()
@ -43,11 +48,20 @@ impl Plugin for PhysicsPlugin {
/// Move the entity with the given acceleration while handling friction,
/// gravity, collisions, and some other stuff.
fn travel(
mut query: Query<(&mut Physics, &mut Position, &Attributes, &WorldName), With<Local>>,
world_container: Res<InstanceContainer>,
mut query: Query<
(
&mut Physics,
&mut LookDirection,
&mut Position,
&Attributes,
&WorldName,
),
With<Local>,
>,
instance_container: Res<InstanceContainer>,
) {
for (mut physics, mut position, attributes, world_name) in &mut query {
let world_lock = world_container
for (mut physics, direction, mut position, attributes, world_name) in &mut query {
let world_lock = instance_container
.get(world_name)
.expect("All entities should be in a valid world");
let world = world_lock.read();
@ -85,6 +99,7 @@ fn travel(
block_friction,
&world,
&mut physics,
&direction,
&mut position,
attributes,
);
@ -158,13 +173,21 @@ pub fn ai_step(
pub struct ForceJumpEvent(pub Entity);
pub fn force_jump_listener(
mut query: Query<(&mut Physics, &Position, &Sprinting, &WorldName)>,
world_container: Res<InstanceContainer>,
mut query: Query<(
&mut Physics,
&Position,
&LookDirection,
&Sprinting,
&WorldName,
)>,
instance_container: Res<InstanceContainer>,
mut events: EventReader<ForceJumpEvent>,
) {
for event in events.iter() {
if let Ok((mut physics, position, sprinting, world_name)) = query.get_mut(event.0) {
let world_lock = world_container
if let Ok((mut physics, position, direction, sprinting, world_name)) =
query.get_mut(event.0)
{
let world_lock = instance_container
.get(world_name)
.expect("All entities should be in a valid world");
let world = world_lock.read();
@ -178,7 +201,7 @@ pub fn force_jump_listener(
};
if **sprinting {
// sprint jumping gives some extra velocity
let y_rot = physics.y_rot * 0.017453292;
let y_rot = direction.y_rot * 0.017453292;
physics.delta += Vec3 {
x: (-f32::sin(y_rot) * 0.2) as f64,
y: 0.,
@ -204,11 +227,13 @@ fn handle_relative_friction_and_calculate_movement(
block_friction: f32,
world: &Instance,
physics: &mut Physics,
direction: &LookDirection,
position: &mut Position,
attributes: &Attributes,
) -> Vec3 {
move_relative(
physics,
direction,
get_friction_influenced_speed(physics, attributes, block_friction),
&Vec3 {
x: physics.xxa as f64,

View file

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

View file

@ -1,12 +1,12 @@
use azalea_buf::McBuf;
use azalea_core::Slot;
use azalea_inventory::ItemSlot;
use azalea_protocol_macros::ClientboundGamePacket;
#[derive(Clone, Debug, McBuf, ClientboundGamePacket)]
pub struct ClientboundContainerSetContentPacket {
pub container_id: u8,
pub container_id: i8,
#[var]
pub state_id: u32,
pub items: Vec<Slot>,
pub carried_item: Slot,
pub items: Vec<ItemSlot>,
pub carried_item: ItemSlot,
}

View file

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

View file

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

View file

@ -1,6 +1,6 @@
use self::registry::RegistryHolder;
use azalea_buf::McBuf;
use azalea_core::{GameType, GlobalPos, OptionalGameType, ResourceLocation};
use azalea_core::{GameMode, GlobalPos, OptionalGameType, ResourceLocation};
use azalea_protocol_macros::ClientboundGamePacket;
/// The first packet sent by the server to the client after login.
@ -11,7 +11,7 @@ use azalea_protocol_macros::ClientboundGamePacket;
pub struct ClientboundLoginPacket {
pub player_id: u32,
pub hardcore: bool,
pub game_type: GameType,
pub game_type: GameMode,
pub previous_game_type: OptionalGameType,
pub levels: Vec<ResourceLocation>,
pub registry_holder: RegistryHolder,

View file

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

View file

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

View file

@ -3,7 +3,7 @@ use azalea_buf::{
BufReadError, McBuf, McBufReadable, McBufVarReadable, McBufVarWritable, McBufWritable,
};
use azalea_chat::FormattedText;
use azalea_core::{FixedBitSet, GameType};
use azalea_core::{FixedBitSet, GameMode};
use azalea_protocol_macros::ClientboundGamePacket;
use std::{
collections::HashMap,
@ -24,7 +24,7 @@ pub struct PlayerInfoEntry {
pub profile: GameProfile,
pub listed: bool,
pub latency: i32,
pub game_mode: GameType,
pub game_mode: GameMode,
pub display_name: Option<FormattedText>,
pub chat_session: Option<RemoteChatSessionData>,
}
@ -40,7 +40,7 @@ pub struct InitializeChatAction {
}
#[derive(Clone, Debug, McBuf)]
pub struct UpdateGameModeAction {
pub game_mode: GameType,
pub game_mode: GameMode,
}
#[derive(Clone, Debug, McBuf)]
pub struct UpdateListedAction {

View file

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

View file

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

View file

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

View file

@ -1,7 +1,8 @@
use azalea_buf::{
BufReadError, McBuf, McBufReadable, McBufVarReadable, McBufVarWritable, McBufWritable,
};
use azalea_core::{ResourceLocation, Slot};
use azalea_core::ResourceLocation;
use azalea_inventory::ItemSlot;
use azalea_protocol_macros::ClientboundGamePacket;
use azalea_registry::RecipeSerializer;
@ -26,7 +27,7 @@ pub struct ShapelessRecipe {
pub group: String,
pub category: CraftingBookCategory,
pub ingredients: Vec<Ingredient>,
pub result: Slot,
pub result: ItemSlot,
}
#[derive(Clone, Debug)]
pub struct ShapedRecipe {
@ -35,7 +36,7 @@ pub struct ShapedRecipe {
pub group: String,
pub category: CraftingBookCategory,
pub ingredients: Vec<Ingredient>,
pub result: Slot,
pub result: ItemSlot,
pub show_notification: bool,
}
@ -71,7 +72,7 @@ impl McBufReadable for ShapedRecipe {
for _ in 0..width * height {
ingredients.push(Ingredient::read_from(buf)?);
}
let result = Slot::read_from(buf)?;
let result = ItemSlot::read_from(buf)?;
let show_notification = bool::read_from(buf)?;
Ok(ShapedRecipe {
@ -91,7 +92,7 @@ pub struct CookingRecipe {
pub group: String,
pub category: CraftingBookCategory,
pub ingredient: Ingredient,
pub result: Slot,
pub result: ItemSlot,
pub experience: f32,
#[var]
pub cooking_time: u32,
@ -100,13 +101,13 @@ pub struct CookingRecipe {
pub struct StoneCutterRecipe {
pub group: String,
pub ingredient: Ingredient,
pub result: Slot,
pub result: ItemSlot,
}
#[derive(Clone, Debug, McBuf)]
pub struct SmithingRecipe {
pub base: Ingredient,
pub addition: Ingredient,
pub result: Slot,
pub result: ItemSlot,
}
#[derive(Clone, Debug, McBuf)]
@ -119,7 +120,7 @@ pub struct SmithingTransformRecipe {
pub template: Ingredient,
pub base: Ingredient,
pub addition: Ingredient,
pub result: Slot,
pub result: ItemSlot,
}
#[derive(Clone, Debug, McBuf)]
@ -159,7 +160,7 @@ pub enum RecipeData {
#[derive(Clone, Debug, McBuf)]
pub struct Ingredient {
pub allowed: Vec<Slot>,
pub allowed: Vec<ItemSlot>,
}
impl McBufWritable for Recipe {

View file

@ -1,5 +1,5 @@
use azalea_buf::McBuf;
use azalea_core::Slot;
use azalea_inventory::{operations::ClickType, ItemSlot};
use azalea_protocol_macros::ServerboundGamePacket;
use std::collections::HashMap;
@ -8,20 +8,9 @@ pub struct ServerboundContainerClickPacket {
pub container_id: u8,
#[var]
pub state_id: u32,
pub slot_num: u16,
pub slot_num: i16,
pub button_num: u8,
pub click_type: ClickType,
pub changed_slots: HashMap<u16, Slot>,
pub carried_item: Slot,
}
#[derive(McBuf, Clone, Copy, Debug)]
pub enum ClickType {
Pickup = 0,
QuickMove = 1,
Swap = 2,
Clone = 3,
Throw = 4,
QuickCraft = 5,
PickupAll = 6,
pub changed_slots: HashMap<u16, ItemSlot>,
pub carried_item: ItemSlot,
}

View file

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

View file

@ -7,20 +7,26 @@ use std::io::{Cursor, Write};
#[derive(Clone, Debug, McBuf, ServerboundGamePacket)]
pub struct ServerboundUseItemOnPacket {
pub hand: InteractionHand,
pub block_hit: BlockHitResult,
pub block_hit: BlockHit,
#[var]
pub sequence: u32,
}
#[derive(Clone, Debug)]
pub struct BlockHitResult {
pub struct BlockHit {
/// The block that we clicked.
pub block_pos: BlockPos,
/// The face of the block that was clicked.
pub direction: Direction,
/// The exact coordinates of the world where the block was clicked. In the
/// network, this is transmitted as the difference between the location and
/// block position.
pub location: Vec3,
/// Whether the player's head is inside of a block.
pub inside: bool,
}
impl McBufWritable for BlockHitResult {
impl McBufWritable for BlockHit {
fn write_into(&self, buf: &mut impl Write) -> Result<(), std::io::Error> {
self.block_pos.write_into(buf)?;
self.direction.write_into(buf)?;
@ -41,7 +47,7 @@ impl McBufWritable for BlockHitResult {
}
}
impl McBufReadable for BlockHitResult {
impl McBufReadable for BlockHit {
fn read_from(buf: &mut Cursor<&[u8]>) -> Result<Self, BufReadError> {
let block_pos = BlockPos::read_from(buf)?;
let direction = Direction::read_from(buf)?;

File diff suppressed because it is too large Load diff

View file

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

View file

@ -1,3 +1 @@
# Azalea World
The Minecraft world representation used in Azalea.

View file

@ -8,7 +8,7 @@ use std::{
sync::{Arc, Weak},
};
use crate::{ChunkStorage, Instance};
use crate::{entity::WorldName, ChunkStorage, Instance};
/// A container of [`Instance`]s (aka worlds). Instances are stored as a Weak
/// pointer here, so if no clients are using an instance it will be forgotten.
@ -37,7 +37,7 @@ impl InstanceContainer {
}
/// Get a world from the container.
pub fn get(&self, name: &ResourceLocation) -> Option<Arc<RwLock<Instance>>> {
pub fn get(&self, name: &WorldName) -> Option<Arc<RwLock<Instance>>> {
self.worlds.get(name).and_then(|world| world.upgrade())
}

View file

@ -4,7 +4,8 @@ use azalea_buf::{
BufReadError, McBuf, McBufReadable, McBufVarReadable, McBufVarWritable, McBufWritable,
};
use azalea_chat::FormattedText;
use azalea_core::{BlockPos, Direction, GlobalPos, Particle, Slot, Vec3};
use azalea_core::{BlockPos, Direction, GlobalPos, Particle, Vec3};
use azalea_inventory::ItemSlot;
use bevy_ecs::component::Component;
use derive_more::Deref;
use enum_as_inner::EnumAsInner;
@ -60,7 +61,7 @@ pub enum EntityDataValue {
String(String),
FormattedText(FormattedText),
OptionalFormattedText(Option<FormattedText>),
ItemStack(Slot),
ItemStack(ItemSlot),
Boolean(bool),
Rotations(Rotations),
BlockPos(BlockPos),

View file

@ -29,7 +29,7 @@ use std::{
};
use uuid::Uuid;
use super::Local;
use super::{Local, LookDirection};
/// A Bevy [`SystemSet`] for various types of entity updates.
#[derive(SystemSet, Debug, Hash, Eq, PartialEq, Clone)]
@ -75,6 +75,7 @@ impl Plugin for EntityPlugin {
debug_detect_updates_received_on_local_entities,
add_dead,
update_bounding_box,
clamp_look_direction,
))
.init_resource::<EntityInfos>();
}
@ -218,10 +219,10 @@ fn update_entity_chunk_positions(
),
Changed<entity::Position>,
>,
world_container: Res<InstanceContainer>,
instance_container: Res<InstanceContainer>,
) {
for (entity, pos, last_pos, world_name) in query.iter_mut() {
let world_lock = world_container.get(world_name).unwrap();
let world_lock = instance_container.get(world_name).unwrap();
let mut world = world_lock.write();
let old_chunk = ChunkPos::from(*last_pos);
@ -285,11 +286,11 @@ fn debug_detect_updates_received_on_local_entities(
fn remove_despawned_entities_from_indexes(
mut commands: Commands,
mut entity_infos: ResMut<EntityInfos>,
world_container: Res<InstanceContainer>,
instance_container: Res<InstanceContainer>,
query: Query<(Entity, &EntityUuid, &Position, &WorldName, &LoadedBy), Changed<LoadedBy>>,
) {
for (entity, uuid, position, world_name, loaded_by) in &query {
let world_lock = world_container.get(world_name).unwrap();
let world_lock = instance_container.get(world_name).unwrap();
let mut world = world_lock.write();
// if the entity has no references left, despawn it
@ -322,6 +323,13 @@ fn remove_despawned_entities_from_indexes(
}
}
pub fn clamp_look_direction(mut query: Query<&mut LookDirection>) {
for mut look_direction in &mut query {
look_direction.y_rot %= 360.0;
look_direction.x_rot = look_direction.x_rot.clamp(-90.0, 90.0) % 360.0;
}
}
impl Debug for EntityInfos {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("EntityInfos").finish()

View file

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

View file

@ -23,7 +23,8 @@ pub use data::*;
use derive_more::{Deref, DerefMut};
pub use dimensions::{update_bounding_box, EntityDimensions};
pub use info::{
EntityInfos, EntityPlugin, EntityUpdateSet, LoadedBy, PartialEntityInfos, RelativeEntityUpdate,
clamp_look_direction, EntityInfos, EntityPlugin, EntityUpdateSet, LoadedBy, PartialEntityInfos,
RelativeEntityUpdate,
};
use std::fmt::Debug;
use uuid::Uuid;
@ -38,19 +39,18 @@ impl std::hash::Hash for MinecraftEntityId {
}
}
impl nohash_hasher::IsEnabled for MinecraftEntityId {}
pub fn set_rotation(physics: &mut Physics, y_rot: f32, x_rot: f32) {
physics.y_rot = y_rot % 360.0;
physics.x_rot = x_rot.clamp(-90.0, 90.0) % 360.0;
// TODO: minecraft also sets yRotO and xRotO to xRot and yRot ... but
// idk what they're used for so
}
pub fn move_relative(physics: &mut Physics, speed: f32, acceleration: &Vec3) {
let input_vector = input_vector(physics, speed, acceleration);
pub fn move_relative(
physics: &mut Physics,
direction: &LookDirection,
speed: f32,
acceleration: &Vec3,
) {
let input_vector = input_vector(direction, speed, acceleration);
physics.delta += input_vector;
}
pub fn input_vector(physics: &mut Physics, speed: f32, acceleration: &Vec3) -> Vec3 {
pub fn input_vector(direction: &LookDirection, speed: f32, acceleration: &Vec3) -> Vec3 {
let distance = acceleration.length_squared();
if distance < 1.0E-7 {
return Vec3::default();
@ -61,8 +61,8 @@ pub fn input_vector(physics: &mut Physics, speed: f32, acceleration: &Vec3) -> V
*acceleration
}
.scale(speed as f64);
let y_rot = f32::sin(physics.y_rot * 0.017453292f32);
let x_rot = f32::cos(physics.y_rot * 0.017453292f32);
let y_rot = f32::sin(direction.y_rot * 0.017453292f32);
let x_rot = f32::cos(direction.y_rot * 0.017453292f32);
Vec3 {
x: acceleration.x * (x_rot as f64) - acceleration.z * (y_rot as f64),
y: acceleration.y,
@ -70,6 +70,20 @@ pub fn input_vector(physics: &mut Physics, speed: f32, acceleration: &Vec3) -> V
}
}
pub fn view_vector(look_direction: &LookDirection) -> Vec3 {
let x_rot = look_direction.x_rot * 0.017453292;
let y_rot = -look_direction.y_rot * 0.017453292;
let y_rot_cos = f32::cos(y_rot);
let y_rot_sin = f32::sin(y_rot);
let x_rot_cos = f32::cos(x_rot);
let x_rot_sin = f32::sin(x_rot);
Vec3 {
x: (y_rot_sin * x_rot_cos) as f64,
y: (-x_rot_sin) as f64,
z: (y_rot_cos * x_rot_cos) as f64,
}
}
/// Get the position of the block below the entity, but a little lower.
pub fn on_pos_legacy(chunk_storage: &ChunkStorage, position: &Position) -> BlockPos {
on_pos(0.2, chunk_storage, position)
@ -128,6 +142,11 @@ impl Debug for EntityUuid {
/// automatically.
#[derive(Component, Clone, Copy, Debug, Default, PartialEq, Deref, DerefMut)]
pub struct Position(Vec3);
impl From<&Position> for Vec3 {
fn from(value: &Position) -> Self {
value.0
}
}
impl From<Position> for ChunkPos {
fn from(value: Position) -> Self {
ChunkPos::from(&value.0)
@ -149,9 +168,14 @@ impl From<&Position> for BlockPos {
}
}
/// The last position of the entity that was sent to the network.
/// The last position of the entity that was sent over the network.
#[derive(Component, Clone, Copy, Debug, Default, PartialEq, Deref, DerefMut)]
pub struct LastSentPosition(Vec3);
impl From<&LastSentPosition> for Vec3 {
fn from(value: &LastSentPosition) -> Self {
value.0
}
}
impl From<LastSentPosition> for ChunkPos {
fn from(value: LastSentPosition) -> Self {
ChunkPos::from(&value.0)
@ -182,9 +206,16 @@ pub struct WorldName(pub ResourceLocation);
///
/// If this is true, the entity will try to jump every tick. (It's equivalent to
/// the space key being held in vanilla.)
#[derive(Debug, Component, Deref, DerefMut)]
#[derive(Debug, Component, Clone, Deref, DerefMut)]
pub struct Jumping(bool);
/// A component that contains the direction an entity is looking.
#[derive(Debug, Component, Clone, Default)]
pub struct LookDirection {
pub x_rot: f32,
pub y_rot: f32,
}
/// The physics data relating to the entity, such as position, velocity, and
/// bounding box.
#[derive(Debug, Component)]
@ -198,12 +229,6 @@ pub struct Physics {
/// Z acceleration.
pub zza: f32,
pub x_rot: f32,
pub y_rot: f32,
pub x_rot_last: f32,
pub y_rot_last: f32,
pub on_ground: bool,
pub last_on_ground: bool,
@ -237,10 +262,38 @@ pub fn add_dead(mut commands: Commands, query: Query<(Entity, &Health), Changed<
}
}
/// A component that contains the offset of the entity's eyes from the entity
/// coordinates.
///
/// This is used to calculate the camera position for players, when spectating
/// an entity, and when raytracing from the entity.
#[derive(Component, Clone, Copy, Debug, PartialEq, Deref, DerefMut)]
pub struct EyeHeight(f32);
impl From<EyeHeight> for f32 {
fn from(value: EyeHeight) -> Self {
value.0
}
}
impl From<EyeHeight> for f64 {
fn from(value: EyeHeight) -> Self {
value.0 as f64
}
}
impl From<&EyeHeight> for f32 {
fn from(value: &EyeHeight) -> Self {
value.0
}
}
impl From<&EyeHeight> for f64 {
fn from(value: &EyeHeight) -> Self {
value.0 as f64
}
}
/// A component NewType for [`azalea_registry::EntityKind`].
///
/// Most of the time, you should be using `azalea_registry::EntityKind`
/// instead.
/// directly instead.
#[derive(Component, Clone, Copy, Debug, PartialEq, Deref)]
pub struct EntityKind(pub azalea_registry::EntityKind);
@ -254,6 +307,8 @@ pub struct EntityBundle {
pub position: Position,
pub last_sent_position: LastSentPosition,
pub physics: Physics,
pub direction: LookDirection,
pub eye_height: EyeHeight,
pub attributes: Attributes,
pub jumping: Jumping,
}
@ -265,11 +320,12 @@ impl EntityBundle {
kind: azalea_registry::EntityKind,
world_name: ResourceLocation,
) -> Self {
// TODO: get correct entity dimensions by having them codegened somewhere
// TODO: get correct entity dimensions by having them codegen'd somewhere
let dimensions = EntityDimensions {
width: 0.6,
height: 1.8,
};
let eye_height = dimensions.height * 0.85;
Self {
kind: EntityKind(kind),
@ -284,12 +340,6 @@ impl EntityBundle {
yya: 0.,
zza: 0.,
x_rot: 0.,
y_rot: 0.,
y_rot_last: 0.,
x_rot_last: 0.,
on_ground: false,
last_on_ground: false,
@ -299,6 +349,8 @@ impl EntityBundle {
has_impulse: false,
},
eye_height: EyeHeight(eye_height),
direction: LookDirection::default(),
attributes: Attributes {
// TODO: do the correct defaults for everything, some

View file

@ -59,11 +59,11 @@ pub fn deduplicate_entities(
(Changed<MinecraftEntityId>, Without<Local>),
>,
mut loaded_by_query: Query<&mut LoadedBy>,
world_container: Res<InstanceContainer>,
instance_container: Res<InstanceContainer>,
) {
// if this entity already exists, remove it
for (new_entity, id, world_name) in query.iter_mut() {
if let Some(world_lock) = world_container.get(world_name) {
if let Some(world_lock) = instance_container.get(world_name) {
let world = world_lock.write();
if let Some(old_entity) = world.entity_by_id.get(id) {
if old_entity == &new_entity {
@ -104,11 +104,11 @@ pub fn deduplicate_local_entities(
(Entity, &MinecraftEntityId, &WorldName),
(Changed<MinecraftEntityId>, With<Local>),
>,
world_container: Res<InstanceContainer>,
instance_container: Res<InstanceContainer>,
) {
// if this entity already exists, remove the old one
for (new_entity, id, world_name) in query.iter_mut() {
if let Some(world_lock) = world_container.get(world_name) {
if let Some(world_lock) = instance_container.get(world_name) {
let world = world_lock.write();
if let Some(old_entity) = world.entity_by_id.get(id) {
if old_entity == &new_entity {
@ -154,11 +154,11 @@ pub fn update_uuid_index(
// mut commands: Commands,
// partial_entity_infos: &mut PartialEntityInfos,
// chunk: &ChunkPos,
// world_container: &WorldContainer,
// instance_container: &WorldContainer,
// world_name: &WorldName,
// mut query: Query<(&MinecraftEntityId, &mut ReferenceCount)>,
// ) {
// let world_lock = world_container.get(world_name).unwrap();
// let world_lock = instance_container.get(world_name).unwrap();
// let world = world_lock.read();
// if let Some(entities) = world.entities_by_chunk.get(chunk).cloned() {
@ -195,6 +195,12 @@ impl Instance {
///
/// Note that this is sorted by `x+y+z` and not `x^2+y^2+z^2`, for
/// optimization purposes.
///
/// ```
/// # fn example(client: &azalea_client::Client) {
/// client.world().read().find_block(client.position(), &azalea_registry::Block::Chest.into());
/// # }
/// ```
pub fn find_block(
&self,
nearest_to: impl Into<BlockPos>,
@ -290,10 +296,10 @@ pub fn update_entity_by_id_index(
(Entity, &MinecraftEntityId, &WorldName, Option<&Local>),
Changed<MinecraftEntityId>,
>,
world_container: Res<InstanceContainer>,
instance_container: Res<InstanceContainer>,
) {
for (entity, id, world_name, local) in query.iter_mut() {
let world_lock = world_container.get(world_name).unwrap();
let world_lock = instance_container.get(world_name).unwrap();
let mut world = world_lock.write();
if local.is_none() {
if let Some(old_entity) = world.entity_by_id.get(id) {

View file

@ -18,6 +18,7 @@ azalea-block = { version = "0.6.0", path = "../azalea-block" }
azalea-chat = { version = "0.6.0", path = "../azalea-chat" }
azalea-client = { version = "0.6.0", path = "../azalea-client" }
azalea-core = { version = "0.6.0", path = "../azalea-core" }
azalea-inventory = { version = "0.1.0", path = "../azalea-inventory" }
azalea-physics = { version = "0.6.0", path = "../azalea-physics" }
azalea-protocol = { version = "0.6.0", path = "../azalea-protocol" }
azalea-registry = { version = "0.6.0", path = "../azalea-registry" }

76
azalea/examples/steal.rs Normal file
View file

@ -0,0 +1,76 @@
//! Steal all the diamonds from all the nearby chests.
use azalea::{prelude::*, BlockPos};
use azalea_inventory::operations::QuickMoveClick;
use azalea_inventory::ItemSlot;
use parking_lot::Mutex;
use std::sync::Arc;
#[tokio::main]
async fn main() {
let account = Account::offline("bot");
// or let bot = Account::microsoft("email").await;
ClientBuilder::new()
.set_handler(handle)
.start(account, "localhost")
.await
.unwrap();
}
#[derive(Default, Clone, Component)]
struct State {
pub checked_chests: Arc<Mutex<Vec<BlockPos>>>,
}
async fn handle(mut bot: Client, event: Event, state: State) -> anyhow::Result<()> {
match event {
Event::Chat(m) => {
if m.username() == Some(bot.profile.name.clone()) {
return Ok(());
};
if m.content() != "go" {
return Ok(());
}
{
state.checked_chests.lock().clear();
}
let chest_block = bot
.world()
.read()
.find_block(bot.position(), &azalea::Block::Chest.into());
// TODO: update this when find_blocks is implemented
let Some(chest_block) = chest_block else {
bot.chat("No chest found");
return Ok(());
};
// bot.goto(BlockPosGoal::from(chest_block));
let Some(chest) = bot.open_container(chest_block).await else {
println!("Couldn't open chest");
return Ok(());
};
println!("Getting contents");
for (index, slot) in chest
.contents()
.expect("we just opened the chest")
.iter()
.enumerate()
{
println!("Checking slot {index}: {slot:?}");
if let ItemSlot::Present(item) = slot {
if item.kind == azalea::Item::Diamond {
println!("clicking slot ^");
chest.click(QuickMoveClick::Left { slot: index as u16 });
}
}
}
println!("Done");
}
_ => {}
}
Ok(())
}

View file

@ -4,7 +4,9 @@
use azalea::ecs::query::With;
use azalea::entity::metadata::Player;
use azalea::entity::Position;
use azalea::entity::{EyeHeight, Position};
use azalea::interact::HitResultComponent;
use azalea::inventory::ItemSlot;
use azalea::pathfinder::BlockPosGoal;
use azalea::{prelude::*, swarm::prelude::*, BlockPos, GameProfileComponent, WalkDirection};
use azalea::{Account, Client, Event};
@ -46,7 +48,7 @@ async fn main() -> anyhow::Result<()> {
let mut accounts = Vec::new();
let mut states = Vec::new();
for i in 0..5 {
for i in 0..1 {
accounts.push(Account::offline(&format!("bot{i}")));
states.push(State::default());
}
@ -112,7 +114,7 @@ async fn handle(mut bot: Client, event: Event, _state: State) -> anyhow::Result<
bot.chat(&format!("You're at {pos:?}",));
}
"whereareyou" => {
let pos = bot.component::<Position>();
let pos = bot.position();
bot.chat(&format!("I'm at {pos:?}",));
}
"goto" => {
@ -122,10 +124,11 @@ async fn handle(mut bot: Client, event: Event, _state: State) -> anyhow::Result<
bot.goto(BlockPosGoal::from(target_pos));
}
"look" => {
let entity_pos = bot.entity_component::<Position>(entity);
let target_pos: BlockPos = entity_pos.into();
println!("target_pos: {target_pos:?}");
bot.look_at(target_pos.center());
let entity_pos = bot
.entity_component::<Position>(entity)
.up(bot.entity_component::<EyeHeight>(entity).into());
println!("entity_pos: {entity_pos:?}");
bot.look_at(entity_pos);
}
"jump" => {
bot.set_jumping(true);
@ -140,18 +143,21 @@ async fn handle(mut bot: Client, event: Event, _state: State) -> anyhow::Result<
"lag" => {
std::thread::sleep(Duration::from_millis(1000));
}
"inventory" => {
println!("inventory: {:?}", bot.menu());
}
"findblock" => {
let target_pos = bot.world().read().find_block(
bot.component::<Position>(),
&azalea_registry::Block::DiamondBlock.into(),
);
let target_pos = bot
.world()
.read()
.find_block(bot.position(), &azalea::Block::DiamondBlock.into());
bot.chat(&format!("target_pos: {target_pos:?}",));
}
"gotoblock" => {
let target_pos = bot.world().read().find_block(
bot.component::<Position>(),
&azalea_registry::Block::DiamondBlock.into(),
);
let target_pos = bot
.world()
.read()
.find_block(bot.position(), &azalea::Block::DiamondBlock.into());
if let Some(target_pos) = target_pos {
// +1 to stand on top of the block
bot.goto(BlockPosGoal::from(target_pos.up(1)));
@ -159,6 +165,49 @@ async fn handle(mut bot: Client, event: Event, _state: State) -> anyhow::Result<
bot.chat("no diamond block found");
}
}
"lever" => {
let target_pos = bot
.world()
.read()
.find_block(bot.position(), &azalea::Block::Lever.into());
let Some(target_pos) = target_pos else {
bot.chat("no lever found");
return Ok(())
};
bot.goto(BlockPosGoal::from(target_pos));
bot.look_at(target_pos.center());
bot.block_interact(target_pos);
}
"hitresult" => {
let hit_result = bot.get_component::<HitResultComponent>();
bot.chat(&format!("hit_result: {hit_result:?}",));
}
"chest" => {
let target_pos = bot
.world()
.read()
.find_block(bot.position(), &azalea::Block::Chest.into());
let Some(target_pos) = target_pos else {
bot.chat("no chest found");
return Ok(())
};
bot.look_at(target_pos.center());
let container = bot.open_container(target_pos).await;
println!("container: {:?}", container);
if let Some(container) = container {
if let Some(contents) = container.contents() {
for item in contents {
if let ItemSlot::Present(item) = item {
println!("item: {:?}", item);
}
}
} else {
println!("container was immediately closed");
}
} else {
println!("no container found");
}
}
_ => {}
}
}
@ -196,7 +245,7 @@ async fn swarm_handle(
SwarmEvent::Chat(m) => {
println!("swarm chat message: {}", m.message().to_ansi());
if m.message().to_string() == "<py5> world" {
for (name, world) in &swarm.world_container.read().worlds {
for (name, world) in &swarm.instance_container.read().worlds {
println!("world name: {name}");
if let Some(w) = world.upgrade() {
for chunk_pos in w.read().chunks.chunks.values() {

View file

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

View file

@ -1,4 +1,5 @@
use crate::app::{App, CoreSchedule, IntoSystemAppConfig, Plugin, PluginGroup, PluginGroupBuilder};
use crate::container::ContainerPlugin;
use crate::ecs::{
component::Component,
entity::Entity,
@ -9,7 +10,8 @@ use crate::ecs::{
};
use azalea_core::Vec3;
use azalea_physics::{force_jump_listener, PhysicsSet};
use azalea_world::entity::{metadata::Player, set_rotation, Jumping, Local, Physics, Position};
use azalea_world::entity::{clamp_look_direction, EyeHeight, LookDirection};
use azalea_world::entity::{metadata::Player, Jumping, Local, Position};
use std::f64::consts::PI;
use crate::pathfinder::PathfinderPlugin;
@ -22,7 +24,9 @@ impl Plugin for BotPlugin {
.add_event::<JumpEvent>()
.add_systems((
insert_bot,
look_at_listener.before(force_jump_listener),
look_at_listener
.before(force_jump_listener)
.before(clamp_look_direction),
jump_listener,
stop_jumping
.in_schedule(CoreSchedule::FixedUpdate)
@ -99,12 +103,13 @@ pub struct LookAtEvent {
}
fn look_at_listener(
mut events: EventReader<LookAtEvent>,
mut query: Query<(&Position, &mut Physics)>,
mut query: Query<(&Position, &EyeHeight, &mut LookDirection)>,
) {
for event in events.iter() {
if let Ok((position, mut physics)) = query.get_mut(event.entity) {
let (y_rot, x_rot) = direction_looking_at(position, &event.position);
set_rotation(&mut physics, y_rot, x_rot);
if let Ok((position, eye_height, mut look_direction)) = query.get_mut(event.entity) {
let (y_rot, x_rot) =
direction_looking_at(&position.up(eye_height.into()), &event.position);
(look_direction.y_rot, look_direction.x_rot) = (y_rot, x_rot);
}
}
}
@ -129,5 +134,6 @@ impl PluginGroup for DefaultBotPlugins {
PluginGroupBuilder::start::<Self>()
.add(BotPlugin)
.add(PathfinderPlugin)
.add(ContainerPlugin)
}
}

140
azalea/src/container.rs Normal file
View file

@ -0,0 +1,140 @@
use std::fmt::Formatter;
use azalea_client::{
inventory::{CloseContainerEvent, ContainerClickEvent, InventoryComponent},
packet_handling::PacketEvent,
Client, TickBroadcast,
};
use azalea_core::BlockPos;
use azalea_inventory::{operations::ClickOperation, ItemSlot, Menu};
use azalea_protocol::packets::game::ClientboundGamePacket;
use bevy_app::{App, Plugin};
use bevy_ecs::{component::Component, prelude::EventReader, system::Commands};
use std::fmt::Debug;
pub struct ContainerPlugin;
impl Plugin for ContainerPlugin {
fn build(&self, app: &mut App) {
app.add_system(handle_menu_opened_event);
}
}
pub trait ContainerClientExt {
async fn open_container(&mut self, pos: BlockPos) -> Option<ContainerHandle>;
}
impl ContainerClientExt for Client {
/// Open a container in the world, like a chest.
///
/// ```
/// # use azalea::prelude::*;
/// # async fn example(mut bot: azalea::Client) {
/// let target_pos = bot
/// .world()
/// .read()
/// .find_block(bot.position(), &azalea::Block::Chest.into());
/// let Some(target_pos) = target_pos else {
/// bot.chat("no chest found");
/// return;
/// };
/// let container = bot.open_container(target_pos).await;
/// # }
/// ```
async fn open_container(&mut self, pos: BlockPos) -> Option<ContainerHandle> {
self.ecs
.lock()
.entity_mut(self.entity)
.insert(WaitingForInventoryOpen);
self.block_interact(pos);
let mut receiver = {
let ecs = self.ecs.lock();
let tick_broadcast = ecs.resource::<TickBroadcast>();
tick_broadcast.subscribe()
};
while receiver.recv().await.is_ok() {
let ecs = self.ecs.lock();
if ecs.get::<WaitingForInventoryOpen>(self.entity).is_none() {
break;
}
}
let ecs = self.ecs.lock();
let inventory = ecs
.get::<InventoryComponent>(self.entity)
.expect("no inventory");
if inventory.id == 0 {
None
} else {
Some(ContainerHandle {
id: inventory.id,
client: self.clone(),
})
}
}
}
/// A handle to the open container. The container will be closed once this is
/// dropped.
pub struct ContainerHandle {
pub id: u8,
client: Client,
}
impl Drop for ContainerHandle {
fn drop(&mut self) {
self.client.ecs.lock().send_event(CloseContainerEvent {
entity: self.client.entity,
id: self.id,
});
}
}
impl Debug for ContainerHandle {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ContainerHandle")
.field("id", &self.id)
.finish()
}
}
impl ContainerHandle {
/// Returns the menu of the container. If the container is closed, this
/// will return `None`.
pub fn menu(&self) -> Option<Menu> {
let ecs = self.client.ecs.lock();
let inventory = ecs
.get::<InventoryComponent>(self.client.entity)
.expect("no inventory");
if inventory.id == self.id {
Some(inventory.container_menu.clone().unwrap())
} else {
None
}
}
/// Returns the item slots in the container, not including the player's
/// inventory. If the container is closed, this will return `None`.
pub fn contents(&self) -> Option<Vec<ItemSlot>> {
self.menu().map(|menu| menu.contents())
}
pub fn click(&self, operation: impl Into<ClickOperation>) {
let operation = operation.into();
self.client.ecs.lock().send_event(ContainerClickEvent {
entity: self.client.entity,
window_id: self.id,
operation,
});
}
}
#[derive(Component, Debug)]
pub struct WaitingForInventoryOpen;
fn handle_menu_opened_event(mut commands: Commands, mut events: EventReader<PacketEvent>) {
for event in events.iter() {
if let ClientboundGamePacket::ContainerSetContent { .. } = event.packet {
commands
.entity(event.entity)
.remove::<WaitingForInventoryOpen>();
}
}
}

View file

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

View file

@ -93,7 +93,7 @@ fn goto_listener(
mut commands: Commands,
mut events: EventReader<GotoEvent>,
mut query: Query<(&Position, &WorldName)>,
world_container: Res<InstanceContainer>,
instance_container: Res<InstanceContainer>,
) {
let thread_pool = AsyncComputeTaskPool::get();
@ -106,7 +106,7 @@ fn goto_listener(
vertical_vel: VerticalVel::None,
};
let world_lock = world_container
let world_lock = instance_container
.get(world_name)
.expect("Entity tried to pathfind but the entity isn't in a valid world");
let end = event.goal.goal_node();

View file

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

View file

@ -37,7 +37,7 @@ pub struct Swarm {
// bot_datas: Arc<Mutex<Vec<(Client, S)>>>,
resolved_address: SocketAddr,
address: ServerAddress,
pub world_container: Arc<RwLock<InstanceContainer>>,
pub instance_container: Arc<RwLock<InstanceContainer>>,
bots_tx: mpsc::UnboundedSender<(Option<Event>, Client)>,
swarm_tx: mpsc::UnboundedSender<SwarmEvent>,
@ -248,7 +248,7 @@ where
// resolve the address
let resolved_address = resolver::resolve_address(&address).await?;
let world_container = Arc::new(RwLock::new(InstanceContainer::default()));
let instance_container = Arc::new(RwLock::new(InstanceContainer::default()));
// we can't modify the swarm plugins after this
let (bots_tx, mut bots_rx) = mpsc::unbounded_channel();
@ -263,7 +263,7 @@ where
resolved_address,
address,
world_container,
instance_container,
bots_tx,

View file

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

View file

@ -12,7 +12,7 @@ DATA_RS_DIR = get_dir_location(
'../azalea-world/src/entity/data.rs')
def generate_metadata_names(burger_dataserializers: dict, mappings: Mappings):
serializer_names = [None] * len(burger_dataserializers)
serializer_names: list[Optional[str]] = [None] * len(burger_dataserializers)
for burger_serializer in burger_dataserializers.values():
print(burger_serializer)
@ -105,7 +105,8 @@ use super::{
SnifferState, VillagerData
};
use azalea_chat::FormattedText;
use azalea_core::{BlockPos, Direction, Particle, Slot, Vec3};
use azalea_core::{BlockPos, Direction, Particle, Vec3};
use azalea_inventory::ItemSlot;
use bevy_ecs::{bundle::Bundle, component::Component};
use derive_more::{Deref, DerefMut};
use thiserror::Error;
@ -425,7 +426,7 @@ impl From<EntityDataValue> for UpdateMetadataError {
elif type_name == 'OptionalUnsignedInt':
default = f'OptionalUnsignedInt(Some({default}))' if default != 'Empty' else 'OptionalUnsignedInt(None)'
elif type_name == 'ItemStack':
default = f'Slot::Present({default})' if default != 'Empty' else 'Slot::Empty'
default = f'ItemSlot::Present({default})' if default != 'Empty' else 'ItemSlot::Empty'
elif type_name == 'BlockState':
default = f'{default}' if default != 'Empty' else 'azalea_block::BlockState::AIR'
elif type_name == 'OptionalBlockState':

View file

@ -0,0 +1,108 @@
from lib.utils import padded_hex, to_snake_case, to_camel_case, get_dir_location
from lib.code.utils import burger_type_to_rust_type, write_packet_file
from lib.mappings import Mappings
from typing import Any, Optional
import os
import re
# The directory where declare_menus! {} is done
inventory_menus_dir = get_dir_location(f'../azalea-inventory/src/lib.rs')
def update_menus(initial_menu_entries: dict[str, Any]):
# new_menus is a dict of { menu_id: { "protocol_id": protocol_id } }
# so convert that into an array where the protocol id is the index and the
# values are enum variant names
new_menus: list[str] = [''] * len(initial_menu_entries)
for menu_id, menu in initial_menu_entries.items():
new_menus[menu['protocol_id']] = menu_name_to_enum_name(menu_id)
new_menus.insert(0, 'Player')
with open(inventory_menus_dir, 'r') as f:
menus_rs = f.read().splitlines()
start_line_index = 0
current_menus = []
in_the_macro = False
for i, line in enumerate(menus_rs):
if line.startswith('declare_menus!'):
in_the_macro = True
start_line_index = i
if in_the_macro:
if line.startswith(' ') and line.endswith('{'):
# get the variant name for this menu
current_menu = line[:-1].strip()
current_menus.append(current_menu)
print('current_menus', current_menus)
print('new_menus', new_menus)
# now we have the current menus, so compare that with the expected
# menus and update the file if needed
if current_menus != new_menus:
# ok so insert the new menus with todo!() for the body
current_menus_list_index = 0
new_menus_list_index = 0
insert_line_index = start_line_index + 1
# figure out what menus need to be placed
while True:
# if the values at the indexes are the same, add to both and don't do anything
if (
current_menus_list_index < len(current_menus)
and new_menus_list_index < len(new_menus)
and current_menus[current_menus_list_index] == new_menus[new_menus_list_index]
):
current_menus_list_index += 1
new_menus_list_index += 1
# increase insert_line_index until we get a line that starts with }
while not menus_rs[insert_line_index].strip().startswith('}'):
insert_line_index += 1
insert_line_index += 1
# print('same', current_menus_list_index,
# new_menus_list_index, insert_line_index)
# something was added to new_menus but not current_menus
elif new_menus_list_index < len(new_menus) and new_menus[new_menus_list_index] not in current_menus:
# insert the new menu
menus_rs.insert(
insert_line_index, f' {new_menus[new_menus_list_index]} {{\n todo!()\n }},')
insert_line_index += 1
new_menus_list_index += 1
print('added', current_menus_list_index,
new_menus_list_index, insert_line_index)
# something was removed from new_menus but is still in current_menus
elif current_menus_list_index < len(current_menus) and current_menus[current_menus_list_index] not in new_menus:
# remove the current menu
while not menus_rs[insert_line_index].strip().startswith('}'):
menus_rs.pop(insert_line_index)
menus_rs.pop(insert_line_index)
current_menus_list_index += 1
print('removed', current_menus_list_index,
new_menus_list_index, insert_line_index)
# if current_menus_list_index overflowed, then add the rest of the new menus
elif current_menus_list_index >= len(current_menus):
for i in range(new_menus_list_index, len(new_menus)):
menus_rs.insert(
insert_line_index, f' {new_menus[i]} {{\n todo!()\n }},')
insert_line_index += 1
print('current_menus_list_index overflowed', current_menus_list_index,
new_menus_list_index, insert_line_index)
break
# if new_menus_list_index overflowed, then remove the rest of the current menus
elif new_menus_list_index >= len(new_menus):
for _ in range(current_menus_list_index, len(current_menus)):
while not menus_rs[insert_line_index].strip().startswith('}'):
menus_rs.pop(insert_line_index)
menus_rs.pop(insert_line_index)
# current_menus_list_index += 1
print('new_menus_list_index overflowed', current_menus_list_index,
new_menus_list_index, insert_line_index)
break
with open(inventory_menus_dir, 'w') as f:
f.write('\n'.join(menus_rs))
def menu_name_to_enum_name(menu_name: str) -> str:
return to_camel_case(menu_name.split(':')[-1])

View file

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

View file

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