add stuff related to chat signing

and also some stuff related to digging because i forgot to do a different branch lol
This commit is contained in:
mat 2023-05-26 15:18:04 -05:00
parent 9bdace4aab
commit 6188230009
13 changed files with 500 additions and 28 deletions

160
Cargo.lock generated
View file

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

View file

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

View file

@ -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:?}");
}

138
azalea-auth/src/certs.rs Normal file
View file

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

View file

@ -2,6 +2,7 @@
mod auth;
mod cache;
pub mod certs;
pub mod game_profile;
pub mod sessionserver;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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