aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormat <git@matdoes.dev>2023-05-26 15:18:04 -0500
committermat <git@matdoes.dev>2023-05-26 15:18:04 -0500
commit6188230009b49f96b755ade32a28b932e7810196 (patch)
treef7b6bc8e25dfda27f3162f9e6bd53721fb3a86cc
parent9bdace4aab064257dccb39fab4d47fde6dd9a062 (diff)
downloadazalea-drasl-6188230009b49f96b755ade32a28b932e7810196.tar.xz
add stuff related to chat signing
and also some stuff related to digging because i forgot to do a different branch lol
-rw-r--r--Cargo.lock160
-rw-r--r--azalea-auth/Cargo.toml5
-rwxr-xr-xazalea-auth/examples/certificates.rs24
-rw-r--r--azalea-auth/src/certs.rs138
-rwxr-xr-xazalea-auth/src/lib.rs1
-rw-r--r--azalea-client/Cargo.toml1
-rwxr-xr-xazalea-client/src/account.rs40
-rw-r--r--azalea-client/src/interact.rs70
-rw-r--r--azalea-client/src/inventory.rs40
-rw-r--r--azalea-client/src/lib.rs1
-rw-r--r--azalea-client/src/mining.rs35
-rw-r--r--azalea-core/src/game_type.rs9
-rw-r--r--azalea-inventory/azalea-inventory-macros/src/menu_impl.rs4
13 files changed, 500 insertions, 28 deletions
diff --git a/Cargo.lock b/Cargo.lock
index c76696cb..ac70b757 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -198,11 +198,14 @@ version = "0.7.0"
dependencies = [
"azalea-buf",
"azalea-crypto",
+ "base64",
"chrono",
"env_logger",
"log",
"num-bigint",
+ "parking_lot",
"reqwest",
+ "rsa",
"serde",
"serde_json",
"thiserror",
@@ -282,6 +285,7 @@ dependencies = [
"azalea-core",
"azalea-crypto",
"azalea-inventory",
+ "azalea-nbt",
"azalea-physics",
"azalea-protocol",
"azalea-registry",
@@ -328,8 +332,10 @@ dependencies = [
"criterion",
"num-bigint",
"rand",
+ "rsa",
"rsa_public_encrypt_pkcs1",
"sha-1",
+ "sha2",
"uuid",
]
@@ -502,9 +508,15 @@ dependencies = [
[[package]]
name = "base64"
-version = "0.21.0"
+version = "0.21.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d"
+
+[[package]]
+name = "base64ct"
+version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a"
+checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
[[package]]
name = "bevy_app"
@@ -802,6 +814,7 @@ checksum = "4e3c5919066adf22df73762e50cffcde3a758f2a848b113b586d1f86728b673b"
dependencies = [
"num-integer",
"num-traits",
+ "serde",
]
[[package]]
@@ -896,6 +909,12 @@ dependencies = [
]
[[package]]
+name = "const-oid"
+version = "0.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "520fbf3c07483f94e3e3ca9d0cfd913d7718ef2483d2cfd91c0d9e91474ab913"
+
+[[package]]
name = "convert_case"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1015,6 +1034,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23d8666cb01533c39dde32bcbab8e227b4ed6679b2c925eba05feabea39508fb"
[[package]]
+name = "der"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56acb310e15652100da43d130af8d97b509e95af61aab1c5a7939ef24337ee17"
+dependencies = [
+ "const-oid",
+ "pem-rfc7468",
+ "zeroize",
+]
+
+[[package]]
name = "derive_more"
version = "0.99.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1034,6 +1064,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f"
dependencies = [
"block-buffer",
+ "const-oid",
"crypto-common",
]
@@ -1541,6 +1572,9 @@ name = "lazy_static"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+dependencies = [
+ "spin",
+]
[[package]]
name = "libc"
@@ -1555,6 +1589,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fc7aa29613bd6a620df431842069224d8bc9011086b1db4c0e0cd47fa03ec9a"
[[package]]
+name = "libm"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7012b1bbb0719e1097c47611d3898568c546d597c2e74d66f6087edd5233ff4"
+
+[[package]]
name = "linked-hash-map"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1696,6 +1736,23 @@ dependencies = [
]
[[package]]
+name = "num-bigint-dig"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2399c9463abc5f909349d8aa9ba080e0b88b3ce2885389b60b993f39b1a56905"
+dependencies = [
+ "byteorder",
+ "lazy_static",
+ "libm 0.2.7",
+ "num-integer",
+ "num-iter",
+ "num-traits",
+ "rand",
+ "smallvec",
+ "zeroize",
+]
+
+[[package]]
name = "num-complex"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1744,6 +1801,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
dependencies = [
"autocfg",
+ "libm 0.2.7",
]
[[package]]
@@ -1796,7 +1854,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1914cd452d8fccd6f9db48147b29fd4ae05bea9dc5d9ad578509f72415de282"
dependencies = [
"cfg-if",
- "libm",
+ "libm 0.1.4",
]
[[package]]
@@ -1832,6 +1890,15 @@ dependencies = [
]
[[package]]
+name = "pem-rfc7468"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412"
+dependencies = [
+ "base64ct",
+]
+
+[[package]]
name = "percent-encoding"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1860,6 +1927,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
+name = "pkcs1"
+version = "0.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f"
+dependencies = [
+ "der",
+ "pkcs8",
+ "spki",
+]
+
+[[package]]
+name = "pkcs8"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
+dependencies = [
+ "der",
+ "spki",
+]
+
+[[package]]
name = "plotters"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2069,6 +2157,29 @@ dependencies = [
]
[[package]]
+name = "rsa"
+version = "0.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ab43bb47d23c1a631b4b680199a45255dce26fa9ab2fa902581f624ff13e6a8"
+dependencies = [
+ "byteorder",
+ "const-oid",
+ "digest",
+ "num-bigint-dig",
+ "num-integer",
+ "num-iter",
+ "num-traits",
+ "pkcs1",
+ "pkcs8",
+ "rand_core",
+ "sha2",
+ "signature",
+ "spki",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
name = "rsa_public_encrypt_pkcs1"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2238,6 +2349,17 @@ dependencies = [
]
[[package]]
+name = "sha2"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
+[[package]]
name = "sharded-slab"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2256,6 +2378,16 @@ dependencies = [
]
[[package]]
+name = "signature"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e1788eed21689f9cf370582dfc467ef36ed9c707f073528ddafa8d83e3b8500"
+dependencies = [
+ "digest",
+ "rand_core",
+]
+
+[[package]]
name = "simple_asn1"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2302,12 +2434,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
[[package]]
+name = "spki"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d1e996ef02c474957d681f1b05213dfb0abab947b446a62d37770b23500184a"
+dependencies = [
+ "base64ct",
+ "der",
+]
+
+[[package]]
name = "static_assertions"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
+name = "subtle"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc"
+
+[[package]]
name = "syn"
version = "1.0.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3014,3 +3162,9 @@ dependencies = [
"quote",
"syn 1.0.109",
]
+
+[[package]]
+name = "zeroize"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9"
diff --git a/azalea-auth/Cargo.toml b/azalea-auth/Cargo.toml
index a795407f..bf557dc9 100644
--- a/azalea-auth/Cargo.toml
+++ b/azalea-auth/Cargo.toml
@@ -11,13 +11,16 @@ version = "0.7.0"
[dependencies]
azalea-buf = { path = "../azalea-buf", version = "^0.7.0" }
azalea-crypto = { path = "../azalea-crypto", version = "^0.7.0" }
-chrono = { version = "0.4.22", default-features = false }
+base64 = "0.21.2"
+chrono = { version = "0.4.22", default-features = false, features = ["serde"] }
log = "0.4.17"
num-bigint = "0.4.3"
+parking_lot = "0.12.1"
reqwest = { version = "0.11.12", default-features = false, features = [
"json",
"rustls-tls",
] }
+rsa = "0.9.2"
serde = { version = "^1.0", features = ["derive"] }
serde_json = "1.0.93"
thiserror = "1.0.37"
diff --git a/azalea-auth/examples/certificates.rs b/azalea-auth/examples/certificates.rs
new file mode 100755
index 00000000..69a38efe
--- /dev/null
+++ b/azalea-auth/examples/certificates.rs
@@ -0,0 +1,24 @@
+use std::path::PathBuf;
+
+#[tokio::main]
+async fn main() {
+ env_logger::init();
+
+ let cache_file = PathBuf::from("example_cache.json");
+
+ let auth_result = azalea_auth::auth(
+ "example@example.com",
+ azalea_auth::AuthOpts {
+ cache_file: Some(cache_file),
+ ..Default::default()
+ },
+ )
+ .await
+ .unwrap();
+
+ let certs = azalea_auth::certs::fetch_certificates(&auth_result.access_token)
+ .await
+ .unwrap();
+
+ println!("{certs:?}");
+}
diff --git a/azalea-auth/src/certs.rs b/azalea-auth/src/certs.rs
new file mode 100644
index 00000000..809a10c6
--- /dev/null
+++ b/azalea-auth/src/certs.rs
@@ -0,0 +1,138 @@
+use base64::Engine;
+use chrono::{DateTime, Utc};
+use rsa::{pkcs8::DecodePrivateKey, RsaPrivateKey};
+use serde::Deserialize;
+use thiserror::Error;
+
+#[derive(Debug, Error)]
+pub enum FetchCertificatesError {
+ #[error("Http error: {0}")]
+ Http(#[from] reqwest::Error),
+ #[error("Couldn't parse pkcs8 private key: {0}")]
+ Pkcs8(#[from] rsa::pkcs8::Error),
+}
+
+/// Fetch the Mojang-provided key-pair for your player, which is used for
+/// cryptographically signing chat messages.
+pub async fn fetch_certificates(
+ minecraft_access_token: &str,
+) -> Result<Certificates, FetchCertificatesError> {
+ let client = reqwest::Client::new();
+
+ let res = client
+ .post("https://api.minecraftservices.com/player/certificates")
+ .header("Authorization", format!("Bearer {minecraft_access_token}"))
+ .send()
+ .await?
+ .json::<CertificatesResponse>()
+ .await?;
+ log::trace!("{:?}", res);
+
+ // using RsaPrivateKey::from_pkcs8_pem gives an error with decoding base64 so we
+ // just decode it ourselves
+
+ // remove the first and last lines of the private key
+ let private_key_pem_base64 = res
+ .key_pair
+ .private_key
+ .lines()
+ .skip(1)
+ .take_while(|line| !line.starts_with('-'))
+ .collect::<String>();
+ let private_key_der = base64::engine::general_purpose::STANDARD
+ .decode(private_key_pem_base64)
+ .unwrap();
+
+ let public_key_pem_base64 = res
+ .key_pair
+ .public_key
+ .lines()
+ .skip(1)
+ .take_while(|line| !line.starts_with('-'))
+ .collect::<String>();
+ let public_key_der = base64::engine::general_purpose::STANDARD
+ .decode(public_key_pem_base64)
+ .unwrap();
+
+ // the private key also contains the public key so it's basically a keypair
+ let key_pair = RsaPrivateKey::from_pkcs8_der(&private_key_der).unwrap();
+
+ let certificates = Certificates {
+ key_pair,
+ key_pair_bytes: KeyPairBytes {
+ private_key: private_key_der,
+ public_key: public_key_der,
+ },
+
+ signature_v1: base64::engine::general_purpose::STANDARD
+ .decode(&res.public_key_signature)
+ .unwrap(),
+ signature_v2: base64::engine::general_purpose::STANDARD
+ .decode(&res.public_key_signature_v2)
+ .unwrap(),
+
+ expires_at: res.expires_at,
+ refresh_after: res.refreshed_after,
+ };
+
+ Ok(certificates)
+}
+
+/// A chat signing certificate.
+#[derive(Clone, Debug)]
+pub struct Certificates {
+ /// The RSA private and public key.
+ pub key_pair: RsaPrivateKey,
+ /// The keypair as DER-encoded bytes.
+ pub key_pair_bytes: KeyPairBytes,
+
+ pub signature_v1: Vec<u8>,
+ pub signature_v2: Vec<u8>,
+
+ pub expires_at: DateTime<Utc>,
+ pub refresh_after: DateTime<Utc>,
+}
+
+/// A keypair as DER-encoded bytes.
+#[derive(Clone, Debug)]
+pub struct KeyPairBytes {
+ pub private_key: Vec<u8>,
+ pub public_key: Vec<u8>,
+}
+
+#[derive(Debug, Deserialize)]
+pub struct CertificatesResponse {
+ #[serde(rename = "keyPair")]
+ pub key_pair: KeyPairResponse,
+
+ /// base64 string; signed data
+ #[serde(rename = "publicKeySignature")]
+ pub public_key_signature: String,
+
+ /// base64 string; signed data
+ #[serde(rename = "publicKeySignatureV2")]
+ pub public_key_signature_v2: String,
+
+ /// Date like `2022-04-30T00:11:32.174783069Z`
+ #[serde(rename = "expiresAt")]
+ pub expires_at: DateTime<Utc>,
+
+ /// Date like `2022-04-29T16:11:32.174783069Z`
+ #[serde(rename = "refreshedAfter")]
+ pub refreshed_after: DateTime<Utc>,
+}
+
+#[derive(Debug, Deserialize)]
+pub struct KeyPairResponse {
+ /// -----BEGIN RSA PRIVATE KEY-----
+ /// ...
+ /// -----END RSA PRIVATE KEY-----
+ #[serde(rename = "privateKey")]
+ pub private_key: String,
+
+ /// -----BEGIN RSA PUBLIC KEY-----
+ /// ...
+ /// -----END RSA PUBLIC KEY-----
+ #[serde(rename = "publicKey")]
+ pub public_key: String,
+}
diff --git a/azalea-auth/src/lib.rs b/azalea-auth/src/lib.rs
index 794332d4..cf0d0401 100755
--- a/azalea-auth/src/lib.rs
+++ b/azalea-auth/src/lib.rs
@@ -2,6 +2,7 @@
mod auth;
mod cache;
+pub mod certs;
pub mod game_profile;
pub mod sessionserver;
diff --git a/azalea-client/Cargo.toml b/azalea-client/Cargo.toml
index 0271d42e..09a682b0 100644
--- a/azalea-client/Cargo.toml
+++ b/azalea-client/Cargo.toml
@@ -13,6 +13,7 @@ anyhow = "1.0.59"
async-trait = "0.1.58"
azalea-auth = { path = "../azalea-auth", version = "0.7.0" }
azalea-block = { path = "../azalea-block", version = "0.7.0" }
+azalea-nbt = { path = "../azalea-nbt", version = "0.7.0" }
azalea-chat = { path = "../azalea-chat", version = "0.7.0" }
azalea-core = { path = "../azalea-core", version = "0.7.0" }
azalea-crypto = { path = "../azalea-crypto", version = "0.7.0" }
diff --git a/azalea-client/src/account.rs b/azalea-client/src/account.rs
index dba2d0f1..12a16493 100755
--- a/azalea-client/src/account.rs
+++ b/azalea-client/src/account.rs
@@ -3,7 +3,9 @@
use std::sync::Arc;
use crate::get_mc_dir;
+use azalea_auth::certs::{Certificates, FetchCertificatesError};
use parking_lot::Mutex;
+use thiserror::Error;
use uuid::Uuid;
/// Something that can join Minecraft servers.
@@ -38,17 +40,22 @@ pub struct Account {
pub uuid: Option<Uuid>,
/// The parameters (i.e. email) that were passed for creating this
- /// [`Account`]. This is used to for automatic reauthentication when we get
+ /// [`Account`]. This is used for automatic reauthentication when we get
/// "Invalid Session" errors. If you don't need that feature (like in
/// offline mode), then you can set this to `AuthOpts::default()`.
pub account_opts: AccountOpts,
+
+ /// The certificates used for chat signing.
+ ///
+ /// This is set when you call [`Self::request_certs`], but you only
+ /// need to if the servers you're joining require it.
+ pub certs: Option<Certificates>,
}
/// The parameters that were passed for creating the associated [`Account`].
#[derive(Clone, Debug)]
pub enum AccountOpts {
Offline { username: String },
- // this is an enum so legacy Mojang auth can be added in the future
Microsoft { email: String },
}
@@ -64,6 +71,7 @@ impl Account {
account_opts: AccountOpts::Offline {
username: username.to_string(),
},
+ certs: None,
}
}
@@ -93,6 +101,8 @@ impl Account {
account_opts: AccountOpts::Microsoft {
email: email.to_string(),
},
+ // we don't do chat signing by default unless the user asks for it
+ certs: None,
})
}
@@ -122,3 +132,29 @@ impl Account {
}
}
}
+
+#[derive(Error, Debug)]
+pub enum RequestCertError {
+ #[error("Failed to fetch certificates")]
+ FetchCertificates(#[from] FetchCertificatesError),
+ #[error("You can't request certificates for an offline account")]
+ NoAccessToken,
+}
+
+impl Account {
+ /// Request the certificates used for chat signing and set it in
+ /// [`Self::certs`].
+ pub async fn request_certs(&mut self) -> Result<(), RequestCertError> {
+ let certs = azalea_auth::certs::fetch_certificates(
+ &self
+ .access_token
+ .as_ref()
+ .ok_or(RequestCertError::NoAccessToken)?
+ .lock(),
+ )
+ .await?;
+ self.certs = Some(certs);
+
+ Ok(())
+ }
+}
diff --git a/azalea-client/src/interact.rs b/azalea-client/src/interact.rs
index fa05aa49..afb55bbf 100644
--- a/azalea-client/src/interact.rs
+++ b/azalea-client/src/interact.rs
@@ -1,4 +1,7 @@
+use azalea_block::BlockState;
use azalea_core::{BlockHitResult, BlockPos, Direction, GameMode, Vec3};
+use azalea_inventory::{ItemSlot, ItemSlotData};
+use azalea_nbt::NbtList;
use azalea_physics::clip::{BlockShapeType, ClipContext, FluidPickType};
use azalea_protocol::packets::game::{
serverbound_interact_packet::InteractionHand,
@@ -20,6 +23,8 @@ use derive_more::{Deref, DerefMut};
use log::warn;
use crate::{
+ client::PlayerAbilities,
+ inventory::InventoryComponent,
local_player::{handle_send_packet_event, LocalGameMode},
Client, LocalPlayer,
};
@@ -193,3 +198,68 @@ pub fn pick(
},
)
}
+
+/// Whether we can't interact with the block, based on your gamemode. If
+/// this is false, then we can interact with the block.
+///
+/// Passing the inventory, block position, and instance is necessary for the
+/// adventure mode check.
+pub fn check_is_interaction_restricted(
+ instance: &Instance,
+ block_pos: &BlockPos,
+ game_mode: &GameMode,
+ inventory: &InventoryComponent,
+) -> bool {
+ match game_mode {
+ GameMode::Adventure => {
+ // vanilla checks for abilities.mayBuild here but servers have no
+ // way of modifying that
+
+ let held_item = inventory.held_item();
+ if let ItemSlot::Present(item) = &held_item {
+ let block = instance.chunks.get_block_state(block_pos);
+ let Some(block) = block else {
+ // block isn't loaded so just say that it is restricted
+ return true;
+ };
+ check_block_can_be_broken_by_item_in_adventure_mode(item, &block)
+ } else {
+ true
+ }
+ }
+ GameMode::Spectator => true,
+ _ => false,
+ }
+}
+
+/// Check if the item has the `CanDestroy` tag for the block.
+pub fn check_block_can_be_broken_by_item_in_adventure_mode(
+ item: &ItemSlotData,
+ block: &BlockState,
+) -> bool {
+ // minecraft caches the last checked block but that's kind of an unnecessary
+ // optimization and makes the code too complicated
+
+ let Some(can_destroy) = item
+ .nbt
+ .as_compound()
+ .and_then(|nbt| nbt.get("tag").and_then(|nbt| nbt.as_compound()))
+ .and_then(|nbt| nbt.get("CanDestroy").and_then(|nbt| nbt.as_list())) else {
+ // no CanDestroy tag
+ return false;
+ };
+
+ let NbtList::String(can_destroy) = can_destroy else {
+ // CanDestroy tag must be a list of strings
+ return false;
+ };
+
+ return false;
+
+ // for block_predicate in can_destroy {
+ // // TODO
+ // // defined in BlockPredicateArgument.java
+ // }
+
+ // true
+}
diff --git a/azalea-client/src/inventory.rs b/azalea-client/src/inventory.rs
index bce629b0..f8c2b2a4 100644
--- a/azalea-client/src/inventory.rs
+++ b/azalea-client/src/inventory.rs
@@ -78,6 +78,9 @@ pub struct InventoryComponent {
pub container_menu: Option<azalea_inventory::Menu>,
/// The item that is currently held by the cursor. `Slot::Empty` if nothing
/// is currently being held.
+ ///
+ /// This is different from [`Self::hotbar_selected_index`], which is the
+ /// item that's selected in the hotbar.
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
@@ -89,12 +92,13 @@ pub struct InventoryComponent {
/// 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;
+
+ /// The index of the item in the hotbar that's currently being held by the
+ /// player. This MUST be in the range 0..9 (not including 9).
+ ///
+ /// In a vanilla client this is changed by pressing the number keys or using
+ /// the scroll wheel.
+ pub selected_hotbar_slot: u8,
}
impl InventoryComponent {
/// Returns a reference to the currently active menu. If a container is open
@@ -272,7 +276,6 @@ impl InventoryComponent {
};
*menu.slot_mut(slot_index as usize).unwrap() =
ItemSlot::Present(new_carried);
- // }
}
}
} else {
@@ -493,6 +496,13 @@ impl InventoryComponent {
self.quick_craft_status = QuickCraftStatusKind::Start;
self.quick_craft_slots.clear();
}
+
+ /// Get the item in the player's hotbar that is currently being held.
+ pub fn held_item(&self) -> ItemSlot {
+ let inventory = &self.inventory_menu;
+ let hotbar_items = &inventory.slots()[inventory.hotbar_slots_range()];
+ hotbar_items[self.selected_hotbar_slot as usize].clone()
+ }
}
fn can_item_quick_replace(
@@ -521,21 +531,6 @@ fn can_item_quick_replace(
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,
@@ -561,6 +556,7 @@ impl Default for InventoryComponent {
quick_craft_status: QuickCraftStatusKind::Start,
quick_craft_kind: QuickCraftKind::Middle,
quick_craft_slots: HashSet::new(),
+ selected_hotbar_slot: 0,
}
}
}
diff --git a/azalea-client/src/lib.rs b/azalea-client/src/lib.rs
index 10a50e92..c47c5e29 100644
--- a/azalea-client/src/lib.rs
+++ b/azalea-client/src/lib.rs
@@ -21,6 +21,7 @@ mod get_mc_dir;
pub mod interact;
pub mod inventory;
mod local_player;
+mod mining;
mod movement;
pub mod packet_handling;
pub mod ping;
diff --git a/azalea-client/src/mining.rs b/azalea-client/src/mining.rs
new file mode 100644
index 00000000..5af9a20b
--- /dev/null
+++ b/azalea-client/src/mining.rs
@@ -0,0 +1,35 @@
+use azalea_core::BlockPos;
+use bevy_app::{App, Plugin};
+use bevy_ecs::prelude::*;
+
+use crate::Client;
+
+/// A plugin that allows clients to break blocks in the world.
+pub struct MinePlugin;
+impl Plugin for MinePlugin {
+ fn build(&self, app: &mut App) {
+ app.add_event::<StartMiningBlockEvent>()
+ .add_system(handle_start_mining_block_event);
+ }
+}
+
+impl Client {
+ /// Start mining a block.
+ pub fn start_mining_block(&self, position: BlockPos) {
+ self.ecs.lock().send_event(StartMiningBlockEvent {
+ entity: self.entity,
+ position,
+ });
+ }
+}
+
+pub struct StartMiningBlockEvent {
+ pub entity: Entity,
+ pub position: BlockPos,
+}
+
+fn handle_start_mining_block_event(mut events: EventReader<StartMiningBlockEvent>) {
+ for event in events.iter() {
+ //
+ }
+}
diff --git a/azalea-core/src/game_type.rs b/azalea-core/src/game_type.rs
index e1a3e19b..8a17ef49 100644
--- a/azalea-core/src/game_type.rs
+++ b/azalea-core/src/game_type.rs
@@ -82,6 +82,15 @@ impl GameMode {
}
}
+impl GameMode {
+ /// Whether the player can't interact with blocks while in this game mode.
+ ///
+ /// (Returns true if you're in adventure or spectator.)
+ pub fn is_block_placing_restricted(&self) -> bool {
+ matches!(self, GameMode::Adventure | GameMode::Spectator)
+ }
+}
+
impl McBufReadable for GameMode {
fn read_from(buf: &mut Cursor<&[u8]>) -> Result<Self, BufReadError> {
let id = u8::read_from(buf)?;
diff --git a/azalea-inventory/azalea-inventory-macros/src/menu_impl.rs b/azalea-inventory/azalea-inventory-macros/src/menu_impl.rs
index 6bb37e8e..194577ad 100644
--- a/azalea-inventory/azalea-inventory-macros/src/menu_impl.rs
+++ b/azalea-inventory/azalea-inventory-macros/src/menu_impl.rs
@@ -177,6 +177,10 @@ pub fn generate(input: &DeclareMenus) -> TokenStream {
}
/// Get the range of slot indexes that contain the player's hotbar. This may be different for each menu.
+ ///
+ /// ```
+ /// let hotbar_items = &inventory.slots()[inventory.hotbar_slots_range()];
+ /// ```
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())