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 = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a" +checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" + +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" [[package]] name = "bevy_app" @@ -802,6 +814,7 @@ checksum = "4e3c5919066adf22df73762e50cffcde3a758f2a848b113b586d1f86728b673b" dependencies = [ "num-integer", "num-traits", + "serde", ] [[package]] @@ -895,6 +908,12 @@ dependencies = [ "wasm-bindgen", ] +[[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" @@ -1014,6 +1033,17 @@ version = "2.3.3" 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" @@ -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" @@ -1554,6 +1588,12 @@ version = "0.1.4" 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" @@ -1695,6 +1735,23 @@ dependencies = [ "num-traits", ] +[[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" @@ -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]] @@ -1831,6 +1889,15 @@ dependencies = [ "windows-sys 0.45.0", ] +[[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" @@ -1859,6 +1926,27 @@ version = "0.1.0" 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" @@ -2068,6 +2156,29 @@ dependencies = [ "winapi", ] +[[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" @@ -2237,6 +2348,17 @@ dependencies = [ "digest", ] +[[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" @@ -2255,6 +2377,16 @@ dependencies = [ "libc", ] +[[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" @@ -2301,12 +2433,28 @@ version = "0.5.2" 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" @@ -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 { + 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::() + .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::(); + 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::(); + 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, + pub signature_v2: Vec, + + pub expires_at: DateTime, + pub refresh_after: DateTime, +} + +/// A keypair as DER-encoded bytes. +#[derive(Clone, Debug)] +pub struct KeyPairBytes { + pub private_key: Vec, + pub public_key: Vec, +} + +#[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, + + /// Date like `2022-04-29T16:11:32.174783069Z` + #[serde(rename = "refreshedAfter")] + pub refreshed_after: DateTime, +} + +#[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, /// 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, } /// 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, /// 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, - // minecraft also has these fields, but i don't - // think they're necessary?: - // private final NonNullList - // 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 quickCraftSlots, int -// quickCraftType, ItemStack itemStack, int var3) { -// switch (quickCraftType) { -// case 0: -// itemStack.setCount(Mth.floor((float) itemStack.getCount() / (float) -// quickCraftSlots.size())); break; -// case 1: -// itemStack.setCount(1); -// break; -// case 2: -// itemStack.setCount(itemStack.getItem().getMaxStackSize()); -// } - -// itemStack.grow(var3); -// } fn get_quick_craft_slot_count( quick_craft_slots: &HashSet, quick_craft_kind: &QuickCraftKind, @@ -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::() + .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) { + 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 { 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 { // hotbar is always last 9 slots in the player's inventory ((*self.player_slots_range().end() - 8)..=*self.player_slots_range().end())