docs: pumpkin-inventory (#1993)

* init

Signed-off-by: illyrius666 <28700752+illyrius666@users.noreply.github.com>

* fixed linting

Signed-off-by: illyrius666 <28700752+illyrius666@users.noreply.github.com>

* fix clippy

Signed-off-by: Illyrius <FitimQ@live.nl>

---------

Signed-off-by: illyrius666 <28700752+illyrius666@users.noreply.github.com>
Signed-off-by: Illyrius <FitimQ@live.nl>
Co-authored-by: Illyrius <FitimQ@live.nl>
This commit is contained in:
Illyrius
2026-04-09 20:48:19 +02:00
committed by GitHub
parent 7c3b41ba32
commit 2751c42bb7
24 changed files with 1247 additions and 142 deletions

View File

@@ -1,3 +1,15 @@
//! Brewing stand screen handler.
//!
//! This module implements the screen handler for brewing stands.
//! Brewing stands have 5 slots:
//! - Slots 0-2: Potion bottles (output/input)
//! - Slot 3: Brewing ingredient
//! - Slot 4: Fuel (blaze powder)
//!
//! The brewing stand has two tracked properties:
//! - Brew time (0-400): Progress of the current brewing operation
//! - Fuel time (0-20): Amount of fuel remaining
use std::{any::Any, pin::Pin, sync::Arc};
use pumpkin_data::tag::Taggable;
@@ -11,13 +23,29 @@ use crate::{
use pumpkin_data::item_stack::ItemStack;
/// Screen handler for the brewing stand.
///
/// Manages the brewing stand's 5 slots and tracks brewing progress
/// and fuel levels.
pub struct BrewingScreenHandler {
/// The brewing stand's inventory (5 slots: 0-2 potions, 3 ingredient, 4 fuel).
inventory: Arc<dyn Inventory>,
/// Core screen handler behavior (slots, sync ID, listeners).
behaviour: ScreenHandlerBehaviour,
/// Delegate for accessing brew time and fuel time properties.
///
/// Property 0: Brew time (0-400), Property 1: Fuel time (0-20).
_property_delegate: Arc<dyn PropertyDelegate>,
}
impl BrewingScreenHandler {
/// Creates a new brewing stand screen handler.
///
/// # Arguments
/// - `sync_id` - The sync ID for client-server matching
/// - `player_inventory` - The player's inventory
/// - `inventory` - The brewing stand's inventory (5 slots)
/// - `property_delegate` - Delegate for accessing brew time and fuel properties
pub async fn new(
sync_id: u8,
player_inventory: Arc<PlayerInventory>,
@@ -28,10 +56,10 @@ impl BrewingScreenHandler {
impl crate::screen_handler::ScreenHandlerListener for BrewingScreenListener {
fn on_property_update<'a>(
&'a self,
screen_handler: &'a crate::screen_handler::ScreenHandlerBehaviour,
screen_handler: &'a ScreenHandlerBehaviour,
property: u8,
value: i32,
) -> Pin<Box<dyn std::future::Future<Output = ()> + Send + 'a>> {
) -> Pin<Box<dyn Future<Output = ()> + Send + 'a>> {
Box::pin(async move {
if let Some(sync_handler) = screen_handler.sync_handler.as_ref() {
sync_handler
@@ -90,11 +118,17 @@ impl ScreenHandler for BrewingScreenHandler {
Box::pin(async move { self.default_on_closed(player).await })
}
/// Quick move logic for brewing stand.
///
/// - Potions (0-2) -> Player inventory
/// - Ingredient (3) -> Player inventory
/// - Fuel (4) -> Player inventory
/// - From player: potions -> potion slots, fuel -> fuel slot, else -> ingredient slot
fn quick_move<'a>(
&'a mut self,
_player: &'a dyn crate::screen_handler::InventoryPlayer,
slot_index: i32,
) -> Pin<Box<dyn std::future::Future<Output = ItemStack> + Send + 'a>> {
) -> Pin<Box<dyn Future<Output = ItemStack> + Send + 'a>> {
Box::pin(async move {
let mut stack_left = ItemStack::EMPTY.clone();
@@ -152,6 +186,9 @@ impl ScreenHandler for BrewingScreenHandler {
}
}
/// Creates a new brewing stand screen handler.
///
/// Factory function used by the server when a player opens a brewing stand.
pub async fn create_brewing(
sync_id: u8,
player_inventory: Arc<PlayerInventory>,

View File

@@ -1,3 +1,11 @@
//! Brewing module.
//!
//! This module handles the brewing stand mechanics:
//! - [`BrewingStandScreenHandler`] - Screen handler for the brewing stand UI
//!
//! The brewing stand allows players to brew potions by combining water bottles
//! with various ingredients.
pub mod brewing_screen_handler;
pub use brewing_screen_handler::create_brewing;

View File

@@ -1,22 +1,62 @@
//! Container click handling.
//!
//! This module processes inventory click packets from the client and converts
//! them into structured click events. It handles all click types:
//! - Mouse clicks (left/right)
//! - Shift-clicks
//! - Hotbar key presses
//! - Drop actions (Q key or clicking outside)
//! - Drag operations (click and drag across slots)
//! - Double-clicks (pickup all)
//!
//! # Click Types
//!
//! The client sends a mode and button value that are decoded into
//! [`ClickType`] variants. See the Minecraft protocol documentation
//! for packet format details.
use crate::InventoryError;
use pumpkin_protocol::java::server::play::SlotActionType;
/// A parsed container click event.
///
/// Contains the slot being clicked and the type of click action.
#[derive(Debug)]
pub struct Click {
/// The slot being clicked (or outside the inventory).
pub slot: Slot,
/// The type of click action (mouse click, shift-click, drag, etc.).
pub click_type: ClickType,
}
/// Button values for normal mouse clicks.
const BUTTON_CLICK_LEFT: i8 = 0;
const BUTTON_CLICK_RIGHT: i8 = 1;
/// Key code for offhand swap (F key by default).
const KEY_CLICK_OFFHAND: i8 = 40;
/// Hotbar slot key range (1-9 keys).
const KEY_CLICK_HOTBAR_START: i8 = 0;
const KEY_CLICK_HOTBAR_END: i8 = 9;
/// Slot index indicating a click outside the inventory.
const SLOT_INDEX_OUTSIDE: i16 = -999;
impl Click {
/// Parses a slot action into a click event.
///
/// # Arguments
/// - `mode` - The action type from the protocol
/// - `button` - The button value from the protocol
/// - `slot` - The slot index (-999 for outside)
///
/// # Returns
/// The parsed click or an error if invalid.
///
/// # Errors
/// Returns [`InventoryError::InvalidSlot`] or [`InventoryError::InvalidPacket`]
/// for malformed input.
pub fn new(mode: &SlotActionType, button: i8, slot: i16) -> Result<Self, InventoryError> {
match mode {
SlotActionType::Pickup => Self::new_normal_click(button, slot),
@@ -111,36 +151,54 @@ impl Click {
}
}
/// The type of click action.
#[derive(Debug)]
pub enum ClickType {
/// Normal mouse click (left or right).
MouseClick(MouseClick),
/// Shift-click to quick-move an item.
ShiftClick,
/// Hotbar key press (1-9 or offhand swap).
KeyClick(KeyClick),
/// Creative mode middle-click (pick block).
CreativePickItem,
/// Drop item (Q key or drop click).
DropType(DropType),
/// Drag items across multiple slots.
MouseDrag { drag_state: MouseDragState },
/// Double-click to gather items.
DoubleClick,
}
/// Normal mouse button clicks.
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum MouseClick {
Left,
Right,
}
/// Hotbar key clicks.
#[derive(Debug)]
pub enum KeyClick {
/// Hotbar slot index (0-8).
Slot(u8),
/// Swap with offhand.
Offhand,
}
/// A slot reference - either a specific slot or outside the inventory.
#[derive(Debug, Copy, Clone)]
pub enum Slot {
Normal(usize),
OutsideInventory,
}
/// Drop action types.
#[derive(Debug)]
pub enum DropType {
/// Drop a single item (Ctrl+Q).
SingleItem,
/// Drop the full stack (Q).
FullStack,
}
@@ -154,15 +212,24 @@ impl DropType {
}
}
/// Mouse drag button types.
#[derive(Debug, PartialEq, Eq)]
pub enum MouseDragType {
/// Left button drag - even distribution.
Left,
/// Right button drag - one item per slot.
Right,
/// Middle button drag - create full stacks (creative only).
Middle,
}
/// Drag operation state.
#[derive(PartialEq, Eq, Debug)]
pub enum MouseDragState {
/// Start of drag - button determines drag type.
Start(MouseDragType),
/// Adding a slot to the drag.
AddSlot(usize),
/// End of drag - apply to all selected slots.
End,
}

View File

@@ -1,3 +1,14 @@
//! Crafting inventory implementation.
//!
//! This module provides a temporary inventory for crafting grids.
//! Crafting inventories are used for:
//! - The 2x2 crafting grid in the player inventory
//! - The 3x3 crafting grid in crafting tables
//! - Other recipe-based crafting mechanisms
//!
//! Unlike regular inventories, crafting grids are typically cleared when
//! the container closes, and their contents are used up when crafting.
use std::sync::Arc;
use std::{any::Any, pin::Pin};
@@ -9,14 +20,41 @@ use pumpkin_world::inventory::{Clearable, Inventory, InventoryFuture};
use super::recipes::RecipeInputInventory;
/// A temporary inventory for crafting grids.
///
/// Crafting inventories hold items arranged in a grid pattern for crafting recipes.
/// The grid dimensions can vary (2x2 for player inventory, 3x3 for crafting table).
///
/// # Usage
///
/// When a player places items in the crafting grid, they are stored here.
/// When the crafting result is taken, the ingredients are consumed from this inventory.
#[derive(Clone)]
pub struct CraftingInventory {
/// Width of the crafting grid (typically 2 or 3).
pub width: u8,
/// Height of the crafting grid (typically 2 or 3).
pub height: u8,
/// Items in the crafting grid, stored row by row.
pub items: Vec<Arc<Mutex<ItemStack>>>,
}
impl CraftingInventory {
/// Creates a new crafting inventory with the given dimensions.
///
/// # Arguments
/// - `width` - Grid width (e.g., 2 for player crafting, 3 for crafting table)
/// - `height` - Grid height (e.g., 2 for player crafting, 3 for crafting table)
///
/// # Examples
///
/// ```rust,ignore
/// // 2x2 player inventory crafting grid
/// let player_crafting = CraftingInventory::new(2, 2);
///
/// // 3x3 crafting table grid
/// let table_crafting = CraftingInventory::new(3, 3);
/// ```
#[must_use]
pub fn new(width: u8, height: u8) -> Self {
Self {

View File

@@ -1,3 +1,19 @@
//! Crafting screen handler implementation.
//!
//! This module provides screen handlers for crafting mechanics:
//! - [`CraftingScreenHandler`] - Trait for crafting screen handlers
//! - [`CraftingTableScreenHandler`] - The 3x3 crafting table UI
//! - [`ResultSlot`] - The special result slot that shows crafted items
//!
//! # Recipe Matching
//!
//! Crafting recipes are matched against the items in the crafting grid.
//! The system supports:
//! - Shaped recipes (specific patterns)
//! - Shapeless recipes (any arrangement)
//! - Transmute recipes (upgrading items)
//! - Special recipes (like decorated pots)
use std::any::Any;
use std::pin::Pin;
use std::sync::Arc;
@@ -21,18 +37,34 @@ use pumpkin_data::tag::Taggable;
use pumpkin_world::inventory::Inventory;
use tokio::sync::Mutex;
/// CraftingResultSlot.java
/// The result slot in a crafting screen.
///
/// Note: This implementation is different from the original Minecraft code.
/// Particularly, it does not have a 'result' inventory, we directly store it in the slot.
/// This slot should be never modified outside. any modifications to it make change in its input.
/// This special slot displays the output of the current crafting recipe.
/// Unlike normal slots, it doesn't store items permanently - it calculates
/// the result dynamically based on the crafting grid contents.
///
/// # Note
///
/// This implementation differs from vanilla Minecraft. Instead of a separate
/// result inventory, we directly store the result in the slot. The slot
/// should never be modified directly - modifications to the crafting grid
/// automatically update the result.
pub struct ResultSlot {
/// The crafting inventory (grid) that provides recipe input.
pub inventory: Arc<dyn RecipeInputInventory>,
/// Protocol ID for this slot (assigned by screen handler).
pub id: AtomicU8,
/// The cached result item stack.
///
/// Updated when the crafting grid changes and a recipe matches.
pub result: Arc<Mutex<ItemStack>>,
/// Cached reference to the last matched recipe for quick re-matching.
recipe_cache: AtomicCell<Option<&'static CraftingRecipeTypes>>,
}
/// Checks if a recipe pattern is symmetrical horizontally.
///
/// Used to try both orientations when matching shaped recipes.
fn is_symmetrical_horizontally(pattern: &'static [&'static str]) -> bool {
let width = pattern.first().map_or(0, |s| s.len());
for row in pattern {
@@ -48,6 +80,10 @@ fn is_symmetrical_horizontally(pattern: &'static [&'static str]) -> bool {
true
}
/// Checks if a crafting recipe matches the current inventory state.
///
/// Tries the recipe at all possible positions in the grid and handles
/// both orientations for asymmetrical recipes.
#[expect(clippy::too_many_lines)]
async fn recipe_matches<'a>(
recipe: &'static CraftingRecipeTypes,
@@ -237,6 +273,7 @@ async fn recipe_matches<'a>(
impl ResultSlot {
//fn stat_crafted(&self, _crafted_amount: u8, _player: &dyn InventoryPlayer) {}
/// Creates a new result slot for the given crafting inventory.
pub fn new(inventory: Arc<dyn RecipeInputInventory>) -> Self {
Self {
inventory,
@@ -312,6 +349,7 @@ impl ResultSlot {
None
}
/// Refills the output slot with the current recipe result.
async fn refill_output(&self) -> ItemStack {
let result = self
.match_recipe()
@@ -452,10 +490,15 @@ impl ScreenHandlerListener for ResultSlot {
}
}
/// Trait for crafting screen handlers.
///
/// Provides common functionality for crafting UIs including slot setup
/// and recipe result management.
// AbstractCraftingScreenHandler.java
pub trait CraftingScreenHandler<I: RecipeInputInventory>:
RecipeFinderScreenHandler + ScreenHandler
{
/// Adds the result slot and crafting grid slots to the screen handler.
fn add_recipe_slots<'a>(
&'a mut self,
crafing_inventory: Arc<dyn RecipeInputInventory>,
@@ -479,13 +522,24 @@ pub trait CraftingScreenHandler<I: RecipeInputInventory>:
}
}
/// Screen handler for the crafting table.
///
/// The crafting table provides a 3x3 crafting grid and displays
/// the result of the current recipe configuration.
// CraftingMenu
pub struct CraftingTableScreenHandler {
/// Core screen handler behavior (slots, sync ID, listeners).
behaviour: ScreenHandlerBehaviour,
/// The 3x3 crafting grid inventory.
crafting_inventory: Arc<dyn RecipeInputInventory>,
}
impl CraftingTableScreenHandler {
/// Creates a new crafting table screen handler.
///
/// # Arguments
/// - `sync_id` - The sync ID for client-server matching
/// - `player_inventory` - The player's inventory
pub async fn new(sync_id: u8, player_inventory: &Arc<PlayerInventory>) -> Self {
let crafting_inventory: Arc<dyn RecipeInputInventory> =
Arc::new(CraftingInventory::new(3, 3));
@@ -510,15 +564,6 @@ impl CraftingTableScreenHandler {
impl RecipeFinderScreenHandler for CraftingTableScreenHandler {}
impl ScreenHandler for CraftingTableScreenHandler {
fn on_closed<'a>(&'a mut self, player: &'a dyn InventoryPlayer) -> ScreenHandlerFuture<'a, ()> {
Box::pin(async move {
self.default_on_closed(player).await;
//TODO: this.craftingResultInventory.clear();
self.drop_inventory(player, self.crafting_inventory.clone())
.await;
})
}
fn as_any(&self) -> &dyn Any {
self
}
@@ -531,6 +576,20 @@ impl ScreenHandler for CraftingTableScreenHandler {
&mut self.behaviour
}
fn on_closed<'a>(&'a mut self, player: &'a dyn InventoryPlayer) -> ScreenHandlerFuture<'a, ()> {
Box::pin(async move {
self.default_on_closed(player).await;
//TODO: this.craftingResultInventory.clear();
self.drop_inventory(player, self.crafting_inventory.clone())
.await;
})
}
/// Quick move logic for crafting table.
///
/// - Result slot (0): Move to player inventory (10-46)
/// - Crafting grid (1-9): Move to player inventory
/// - Player inventory (10-46): Move to crafting grid first, then shuffle within inventory
fn quick_move<'a>(
&'a mut self,
player: &'a dyn InventoryPlayer,

View File

@@ -1,3 +1,10 @@
//! Crafting module.
//!
//! This module handles crafting mechanics including:
//! - [`CraftingInventory`] - Temporary inventory for crafting slots
//! - [`CraftingScreenHandler`] - Screen handler for crafting tables and inventory crafting
//! - [`recipes`] - Recipe matching and crafting result calculation
pub mod crafting_inventory;
pub mod crafting_screen_handler;
pub mod recipes;

View File

@@ -1,18 +1,47 @@
//! Recipe-related types and traits.
//!
//! This module defines the interfaces for recipe handling in crafting systems.
//! It provides traits for screen handlers that can find recipes and inventories
//! that can serve as recipe input.
//!
//! # Recipe System
//!
//! The recipe system involves:
//! - [`RecipeFinderScreenHandler`] - Screen handlers that can find matching recipes
//! - [`RecipeInputInventory`] - Inventories that provide crafting input
//! - [`RecipeMatcher`] - Helper for matching items to recipes
//! - [`RecipeFinder`] - Helper for finding recipes
use pumpkin_world::inventory::Inventory;
/// Helper struct for matching recipe ingredients.
// RecipeMatcher.java
pub struct RecipeMatcher;
/// Helper struct for finding recipes.
// RecipeFinder.java
pub struct RecipeFinder;
/// Trait for screen handlers that can find crafting recipes.
///
/// Screen handlers implementing this trait can search for recipes
/// that match the current input inventory state.
// AbstractRecipeScreenHandle.java
pub trait RecipeFinderScreenHandler {}
/// Trait for inventories that serve as recipe input.
///
/// Crafting grids implement this trait to provide their dimensions
/// and item access for recipe matching.
pub trait RecipeInputInventory: Inventory {
/// Gets the width of the crafting grid.
fn get_width(&self) -> usize;
/// Gets the height of the crafting grid.
fn get_height(&self) -> usize;
//fn get_held_stacks(), Get a lock on the inventory instead
// TODO: Additional methods for recipe input handling
// fn get_held_stacks(), Get a lock on the inventory instead
// createRecipeInput
// createPositionedRecipeInput
}

View File

@@ -1,15 +1,40 @@
//! Double inventory implementation.
//!
//! This module provides a composite inventory that combines two inventories
//! into one. This is used for large containers like double chests, which
//! consist of two single chest inventories viewed as a single 54-slot inventory.
//!
//! The first inventory's slots come first, followed by the second inventory's
//! slots. Operations are delegated to the appropriate underlying inventory
//! based on the slot index.
use std::{any::Any, pin::Pin, sync::Arc};
use pumpkin_data::item_stack::ItemStack;
use pumpkin_world::inventory::{Clearable, Inventory, InventoryFuture};
use tokio::sync::Mutex;
/// A composite inventory combining two inventories.
///
/// Used for double chests and other large containers that span
/// multiple block entities. The combined inventory size is the sum
/// of both inventories' sizes.
pub struct DoubleInventory {
/// The first inventory (lower slot indices, 0 to first.size()-1).
first: Arc<dyn Inventory>,
/// The second inventory (higher slot indices, `first.size()` to total-1).
second: Arc<dyn Inventory>,
}
impl DoubleInventory {
/// Creates a new double inventory.
///
/// # Arguments
/// - `first` - The first inventory (lower slot indices)
/// - `second` - The second inventory (higher slot indices)
///
/// # Returns
/// A shared reference to the new double inventory.
pub fn new(first: Arc<dyn Inventory>, second: Arc<dyn Inventory>) -> Arc<Self> {
Arc::new(Self { first, second })
}
@@ -56,10 +81,6 @@ impl Inventory for DoubleInventory {
})
}
fn get_max_count_per_stack(&self) -> u8 {
self.first.get_max_count_per_stack()
}
fn set_stack(&self, slot: usize, stack: ItemStack) -> InventoryFuture<'_, ()> {
Box::pin(async move {
if slot >= self.first.size() {
@@ -70,11 +91,6 @@ impl Inventory for DoubleInventory {
})
}
fn mark_dirty(&self) {
self.first.mark_dirty();
self.second.mark_dirty();
}
fn on_open(&self) -> InventoryFuture<'_, ()> {
Box::pin(async move {
self.first.on_open().await;
@@ -89,6 +105,15 @@ impl Inventory for DoubleInventory {
})
}
fn get_max_count_per_stack(&self) -> u8 {
self.first.get_max_count_per_stack()
}
fn mark_dirty(&self) {
self.first.mark_dirty();
self.second.mark_dirty();
}
fn is_valid_slot_for(&self, slot: usize, stack: &ItemStack) -> bool {
if slot >= self.first.size() {
self.second

View File

@@ -1,3 +1,16 @@
//! Item drag handler.
//!
//! This module handles the logic for dragging items across multiple inventory slots.
//! When a player clicks and drags with an item, they can distribute it across
//! multiple slots.
//!
//! Drag types:
//! - Left click drag - Evenly distributes items across slots
//! - Right click drag - Places one item in each slot
//! - Middle click drag (creative) - Creates full stacks in each slot (creative only)
//!
//! Note: This implementation is currently disabled/commented out pending completion.
/*
#[derive(Debug, Default)]
pub struct DragHandler(RwLock<HashMap<u64, Arc<Mutex<Drag>>>>);

View File

@@ -1,12 +1,32 @@
//! Entity equipment management.
//!
//! This module handles the storage and management of entity equipment slots,
//! such as armor (head, chest, legs, feet) and off-hand items.
//!
//! Equipment is stored separately from the main inventory and is visible on
//! the entity model (armor is rendered on the player, held items are visible
//! in hands).
use std::{collections::HashMap, sync::Arc};
use pumpkin_data::data_component_impl::EquipmentSlot;
use pumpkin_data::item_stack::ItemStack;
use tokio::sync::Mutex;
/// Equipment storage for an entity.
///
/// Stores items equipped in armor slots (head, chest, legs, feet) and
/// the off-hand slot. Equipment is separate from the main inventory
/// and affects entity appearance and stats.
///
/// See also: [`EquipmentSlot`](EquipmentSlot)
// EntityEquipment.java
#[derive(Clone)]
pub struct EntityEquipment {
/// Map of equipment slots to their equipped items.
///
/// Keys are equipment slot types (head, chest, legs, feet, off-hand).
/// Values are mutex-protected item stacks for thread-safe access.
pub equipment: HashMap<EquipmentSlot, Arc<Mutex<ItemStack>>>,
}
@@ -17,6 +37,7 @@ impl Default for EntityEquipment {
}
impl EntityEquipment {
/// Creates a new empty equipment storage.
#[must_use]
pub fn new() -> Self {
Self {
@@ -24,6 +45,14 @@ impl EntityEquipment {
}
}
/// Equips an item in a slot, returning the previous item.
///
/// # Arguments
/// - `slot` - The equipment slot
/// - `stack` - The item to equip
///
/// # Returns
/// The previously equipped item, or an empty stack if the slot was empty.
pub async fn put(&mut self, slot: &EquipmentSlot, stack: ItemStack) -> ItemStack {
self.equipment
.insert(slot.clone(), Arc::new(Mutex::new(stack)))
@@ -33,6 +62,12 @@ impl EntityEquipment {
.clone()
}
/// Gets or inserts an empty stack for a slot.
///
/// If the slot doesn't exist, creates it with an empty stack.
///
/// # Returns
/// A mutex-protected item stack for this slot.
#[must_use]
pub fn get_or_insert(&mut self, slot: &EquipmentSlot) -> Arc<Mutex<ItemStack>> {
self.equipment
@@ -41,6 +76,10 @@ impl EntityEquipment {
.clone()
}
/// Gets the item in a slot.
///
/// # Returns
/// The equipped item, or an empty stack if nothing is equipped.
#[must_use]
pub fn get(&self, slot: &EquipmentSlot) -> Arc<Mutex<ItemStack>> {
self.equipment
@@ -49,6 +88,7 @@ impl EntityEquipment {
.unwrap_or(Arc::new(Mutex::new(ItemStack::EMPTY.clone())))
}
/// Checks if all equipment slots are empty.
pub async fn is_empty(&self) -> bool {
for stack in self.equipment.values() {
if !stack.lock().await.is_empty() {
@@ -59,9 +99,10 @@ impl EntityEquipment {
true
}
/// Clears all equipped items.
pub fn clear(&mut self) {
self.equipment.clear();
}
// TODO: tick
// TODO: tick - Equipment updates, durability damage, etc.
}

View File

@@ -1,19 +1,32 @@
use thiserror::Error;
/// Errors that can occur during inventory operations.
///
/// These errors represent various failure conditions when handling inventory
/// interactions, such as invalid slot indices, permission issues, or protocol errors.
#[derive(Error, Debug)]
pub enum InventoryError {
/// Failed to acquire a lock on an inventory or slot.
#[error("Unable to lock")]
LockError,
/// The specified slot index is invalid or out of bounds.
#[error("Invalid slot")]
InvalidSlot,
/// A player attempted to interact with a container that is closed.
///
/// The parameter is the player's entity ID.
#[error("Player '{0}' tried to interact with a closed container")]
ClosedContainerInteract(i32),
/// Multiple players attempted to drag items in the same container simultaneously.
#[error("Multiple players dragging in a container at once")]
MultiplePlayersDragging,
/// Drag operation was performed out of order (e.g., end before start).
#[error("Out of order dragging")]
OutOfOrderDragging,
/// The received inventory packet is malformed or invalid.
#[error("Invalid inventory packet")]
InvalidPacket,
/// The player lacks permission to perform this inventory operation.
#[error("Player does not have enough permissions")]
PermissionError,
}

View File

@@ -1,3 +1,21 @@
//! Furnace-like screen handler.
//!
//! This module implements the screen handler for furnace-like blocks:
//! - Furnace
//! - Smoker
//! - Blast Furnace
//!
//! All three share the same 3-slot layout:
//! - Slot 0: Input (item to smelt/cook)
//! - Slot 1: Fuel (coal, charcoal, etc.)
//! - Slot 2: Output (smelted result)
//!
//! The screen handler tracks 4 properties:
//! - Property 0: Fire icon animation (fuel burn time remaining)
//! - Property 1: Maximum fuel burn time
//! - Property 2: Progress arrow (cooking/smelt time)
//! - Property 3: Maximum progress (typically 200 ticks for furnace)
use std::{any::Any, pin::Pin, sync::Arc};
use pumpkin_data::{fuels::is_fuel, item_stack::ItemStack, screen::WindowType};
@@ -17,13 +35,31 @@ use tracing::debug;
use super::furnace_like_slot::{FurnaceLikeSlot, FurnaceLikeSlotType, FurnaceOutputSlot};
/// Screen handler for furnace-like containers.
///
/// Handles the UI for furnaces, smokers, and blast furnaces.
/// These all share the same slot layout and quick-move behavior.
pub struct FurnaceLikeScreenHandler {
/// The furnace's inventory (3 slots: 0 input, 1 fuel, 2 output).
pub inventory: Arc<dyn Inventory>,
/// Container that tracks accumulated smelting experience.
///
/// Experience is awarded to the player when they take items from the output slot.
experience_container: Arc<dyn ExperienceContainer>,
/// Core screen handler behavior (slots, sync ID, properties, listeners).
behaviour: ScreenHandlerBehaviour,
}
impl FurnaceLikeScreenHandler {
/// Creates a new furnace-like screen handler.
///
/// # Arguments
/// - `sync_id` - The sync ID for client-server matching
/// - `player_inventory` - The player's inventory
/// - `inventory` - The furnace's inventory (3 slots)
/// - `property_delegate` - Delegate for accessing furnace properties
/// - `experience_container` - Container that tracks smelting experience
/// - `window_type` - The window type (Furnace, Smoker, or `BlastFurnace`)
pub async fn new(
sync_id: u8,
player_inventory: &Arc<PlayerInventory>,
@@ -73,6 +109,11 @@ impl FurnaceLikeScreenHandler {
handler
}
/// Adds the 3 furnace inventory slots.
///
/// - Slot 0: Input (top)
/// - Slot 1: Fuel (bottom)
/// - Slot 2: Output
fn add_inventory_slots(&mut self) {
self.add_slot(Arc::new(FurnaceLikeSlot::new(
self.inventory.clone(),
@@ -91,13 +132,6 @@ impl FurnaceLikeScreenHandler {
}
impl ScreenHandler for FurnaceLikeScreenHandler {
fn on_closed<'a>(&'a mut self, player: &'a dyn InventoryPlayer) -> ScreenHandlerFuture<'a, ()> {
Box::pin(async move {
self.default_on_closed(player).await;
// TODO: self.inventory.on_closed(player).await;
})
}
fn as_any(&self) -> &dyn Any {
self
}
@@ -110,6 +144,18 @@ impl ScreenHandler for FurnaceLikeScreenHandler {
&mut self.behaviour
}
fn on_closed<'a>(&'a mut self, player: &'a dyn InventoryPlayer) -> ScreenHandlerFuture<'a, ()> {
Box::pin(async move {
self.default_on_closed(player).await;
// TODO: self.inventory.on_closed(player).await;
})
}
/// Quick move logic for furnace-like containers.
///
/// - From furnace slots (0-2): Move to player inventory
/// - Fuel items: Move to fuel slot (1)
/// - Other items: Move to input slot (0)
fn quick_move<'a>(
&'a mut self,
player: &'a dyn InventoryPlayer,

View File

@@ -1,3 +1,11 @@
//! Furnace-like slot implementations.
//!
//! This module provides specialized slot types for furnace-like containers.
//! Furnaces have three slots with specific behaviors:
//! - Input slot (top): Accepts any smeltable item
//! - Fuel slot (bottom): Only accepts fuel items (coal, charcoal, etc.)
//! - Output slot: Cannot receive items, awards experience when items are taken
use std::sync::{Arc, atomic::AtomicU8};
use pumpkin_data::{fuels::is_fuel, item::Item};
@@ -12,13 +20,19 @@ use crate::{
slot::{BoxFuture, Slot},
};
/// Type of furnace slot.
#[derive(Debug, Clone, Copy)]
pub enum FurnaceLikeSlotType {
/// Input slot (top) - accepts items to smelt.
Top = 0,
/// Fuel slot (bottom) - accepts fuel items.
Bottom = 1,
}
/// Slot for furnace input (top) and fuel (bottom)
/// Slot for furnace input or fuel.
///
/// The input slot accepts any item, while the fuel slot only accepts
/// valid fuel items (and empty buckets for lava fuel).
pub struct FurnaceLikeSlot {
pub inventory: Arc<dyn Inventory>,
pub slot_type: FurnaceLikeSlotType,
@@ -27,6 +41,11 @@ pub struct FurnaceLikeSlot {
}
impl FurnaceLikeSlot {
/// Creates a new furnace slot.
///
/// # Arguments
/// - `inventory` - The furnace's inventory
/// - `slot_type` - Whether this is the input (Top) or fuel (Bottom) slot
pub fn new(inventory: Arc<dyn Inventory>, slot_type: FurnaceLikeSlotType) -> Self {
Self {
inventory,
@@ -51,12 +70,10 @@ impl Slot for FurnaceLikeSlot {
.store(id as u8, std::sync::atomic::Ordering::Relaxed);
}
fn mark_dirty(&self) -> BoxFuture<'_, ()> {
Box::pin(async move {
self.inventory.mark_dirty();
})
}
/// Restricts inserts based on slot type.
///
/// - Top slot: accepts any item (smeltables)
/// - Bottom slot: only accepts fuel items and buckets
fn can_insert<'a>(
&'a self,
stack: &'a pumpkin_data::item_stack::ItemStack,
@@ -70,9 +87,19 @@ impl Slot for FurnaceLikeSlot {
}
})
}
fn mark_dirty(&self) -> BoxFuture<'_, ()> {
Box::pin(async move {
self.inventory.mark_dirty();
})
}
}
/// Output slot for furnace that awards experience when items are taken
/// Output slot for furnace-like containers.
///
/// This slot cannot receive items directly (items are placed here by smelting).
/// When items are taken from this slot, the player receives experience
/// based on the smelting recipes used.
pub struct FurnaceOutputSlot {
pub inventory: Arc<dyn Inventory>,
pub experience_container: Arc<dyn ExperienceContainer>,
@@ -80,6 +107,11 @@ pub struct FurnaceOutputSlot {
}
impl FurnaceOutputSlot {
/// Creates a new furnace output slot.
///
/// # Arguments
/// - `inventory` - The furnace's inventory
/// - `experience_container` - Container that tracks accumulated experience
pub fn new(
inventory: Arc<dyn Inventory>,
experience_container: Arc<dyn ExperienceContainer>,
@@ -106,20 +138,7 @@ impl Slot for FurnaceOutputSlot {
.store(id as u8, std::sync::atomic::Ordering::Relaxed);
}
fn mark_dirty(&self) -> BoxFuture<'_, ()> {
Box::pin(async move {
self.inventory.mark_dirty();
})
}
fn can_insert<'a>(
&'a self,
_stack: &'a pumpkin_data::item_stack::ItemStack,
) -> BoxFuture<'a, bool> {
// Cannot insert items into the output slot
Box::pin(async move { false })
}
/// Awards experience when items are taken from this slot.
fn on_take_item<'a>(
&'a self,
player: &'a dyn InventoryPlayer,
@@ -137,4 +156,19 @@ impl Slot for FurnaceOutputSlot {
self.mark_dirty().await;
})
}
/// Output slot cannot receive inserted items.
fn can_insert<'a>(
&'a self,
_stack: &'a pumpkin_data::item_stack::ItemStack,
) -> BoxFuture<'a, bool> {
// Cannot insert items into the output slot
Box::pin(async move { false })
}
fn mark_dirty(&self) -> BoxFuture<'_, ()> {
Box::pin(async move {
self.inventory.mark_dirty();
})
}
}

View File

@@ -1,2 +1,21 @@
//! Furnace-like containers module.
//!
//! This module handles screen handlers for furnace-like blocks:
//! - Furnace
//! - Smoker
//! - Blast Furnace
//!
//! These containers share the same slot layout and behavior:
//! - Slot 0: Input (item to smelt/cook)
//! - Slot 1: Fuel (coal, charcoal, etc.)
//! - Slot 2: Output (smelted/cooked result)
//!
//! # Properties
//!
/// Furnace-like containers track four properties:
/// - Property 0: Fire icon (fuel remaining)
/// - Property 1: Maximum fuel burn time
/// - Property 2: Progress arrow (smelting progress)
/// - Property 3: Maximum progress (typically 200 ticks)
pub mod furnace_like_screen_handler;
pub mod furnace_like_slot;

View File

@@ -1,3 +1,14 @@
//! Generic container screen handler.
//!
//! This module provides a generic screen handler for simple containers like:
//! - Chests (single, double, ender chest)
//! - Hoppers
//! - Dispensers/Droppers
//! - Barrels
//!
//! These containers have a simple grid layout with no special behaviors
//! (no smelting, no crafting, just item storage).
use std::{any::Any, sync::Arc};
use pumpkin_data::{item_stack::ItemStack, screen::WindowType};
@@ -12,6 +23,9 @@ use crate::{
slot::NormalSlot,
};
/// Creates a generic 9x3 container (single chest).
///
/// Used for single chests, ender chests, and similar containers.
pub async fn create_generic_9x3(
sync_id: u8,
player_inventory: &Arc<PlayerInventory>,
@@ -28,6 +42,9 @@ pub async fn create_generic_9x3(
.await
}
/// Creates a generic 9x6 container (double chest).
///
/// Used for double chests and similar large containers.
pub async fn create_generic_9x6(
sync_id: u8,
player_inventory: &Arc<PlayerInventory>,
@@ -44,6 +61,9 @@ pub async fn create_generic_9x6(
.await
}
/// Creates a generic 3x3 container.
///
/// Used for dispensers, droppers, and similar containers.
pub async fn create_generic_3x3(
sync_id: u8,
player_inventory: &Arc<PlayerInventory>,
@@ -60,6 +80,9 @@ pub async fn create_generic_3x3(
.await
}
/// Creates a hopper container (5 slots).
///
/// Hoppers have a single row of 5 slots.
pub async fn create_hopper(
sync_id: u8,
player_inventory: &Arc<PlayerInventory>,
@@ -76,14 +99,31 @@ pub async fn create_hopper(
.await
}
/// Generic container screen handler.
///
/// Handles simple grid-based containers without special behaviors.
/// The container grid is followed by the player's inventory (27 slots + 9 hotbar).
pub struct GenericContainerScreenHandler {
/// The container's inventory.
pub inventory: Arc<dyn Inventory>,
/// Number of rows in the container grid.
pub rows: u8,
/// Number of columns in the container grid.
pub columns: u8,
/// Core screen handler behavior (slots, sync ID, listeners).
behaviour: ScreenHandlerBehaviour,
}
impl GenericContainerScreenHandler {
/// Creates a new generic container screen handler.
///
/// # Arguments
/// - `screen_type` - The window type for this container
/// - `sync_id` - The sync ID for client-server matching
/// - `player_inventory` - The player's inventory
/// - `inventory` - The container's inventory
/// - `rows` - Number of rows in the container
/// - `columns` - Number of columns in the container
async fn new(
screen_type: WindowType,
sync_id: u8,
@@ -109,6 +149,7 @@ impl GenericContainerScreenHandler {
handler
}
/// Adds slots for the container's inventory grid.
fn add_inventory_slots(&mut self) {
for i in 0..self.rows {
for j in 0..self.columns {
@@ -122,13 +163,6 @@ impl GenericContainerScreenHandler {
}
impl ScreenHandler for GenericContainerScreenHandler {
fn on_closed<'a>(&'a mut self, player: &'a dyn InventoryPlayer) -> ScreenHandlerFuture<'a, ()> {
Box::pin(async move {
self.default_on_closed(player).await;
self.inventory.on_close().await;
})
}
fn as_any(&self) -> &dyn Any {
self
}
@@ -141,6 +175,17 @@ impl ScreenHandler for GenericContainerScreenHandler {
&mut self.behaviour
}
fn on_closed<'a>(&'a mut self, player: &'a dyn InventoryPlayer) -> ScreenHandlerFuture<'a, ()> {
Box::pin(async move {
self.default_on_closed(player).await;
self.inventory.on_close().await;
})
}
/// Quick move logic for generic containers.
///
/// - From container: Move to player inventory (end first)
/// - From player inventory: Move to container (start first)
fn quick_move<'a>(
&'a mut self,
_player: &'a dyn InventoryPlayer,

View File

@@ -1,3 +1,35 @@
//! Pumpkin inventory system.
//!
//! This crate provides the inventory management system for the Pumpkin Minecraft server.
//! It handles player inventories, container screens (like chests, furnaces, crafting tables),
//! slot interactions, item dragging, and inventory synchronization between server and client.
//!
//! # Core Concepts
//!
//! - [`Inventory`] - A trait representing any item storage (player inventory, chest, furnace, etc.)
//! - [`ScreenHandler`] - Manages the UI screen for a container, handling slot layout and interactions
//! - [`Slot`] - Represents a single slot in an inventory that can hold items
//! - [`PlayerInventory`] - The player's 36-slot main inventory plus equipment slots
//! - [`SyncHandler`] - Synchronizes inventory state between server and client
//!
//! # Module Structure
//!
//! - [`player`] - Player inventory and screen handler implementations
//! - [`crafting`] - Crafting table and inventory crafting mechanics
//! - [`furnace_like`] - Furnace, smoker, and blast furnace screen handlers
//! - [`brewing`] - Brewing stand handling
//! - [`slot`] - Slot trait and implementations (normal slots, armor slots)
//! - [`container_click`] - Mouse and keyboard click handling
//! - [`drag_handler`] - Item dragging across multiple slots
//! - [`sync_handler`] - Client-server inventory synchronization
//! - [`window_property`] - Container UI properties (furnace progress, enchantment levels, etc.)
//!
//! [`Inventory`]: pumpkin_world::inventory::Inventory
//! [`ScreenHandler`]: screen_handler::ScreenHandler
//! [`Slot`]: slot::Slot
//! [`PlayerInventory`]: PlayerInventory
//! [`SyncHandler`]: sync_handler::SyncHandler
pub mod brewing;
pub mod container_click;
pub mod crafting;
@@ -20,6 +52,13 @@ use pumpkin_data::data_component_impl::EquipmentSlot;
use crate::player::player_inventory::PlayerInventory;
/// Builds a map of slot indices to equipment slots for the player's inventory.
///
/// This creates the mapping between UI slot indices and equipment slots
/// (head, chest, legs, feet, off-hand) used by the player screen handler.
///
/// # Returns
/// A `HashMap` where keys are slot indices and values are the corresponding [`EquipmentSlot`]s.
#[must_use]
pub fn build_equipment_slots() -> HashMap<usize, EquipmentSlot> {
let mut equipment_slots = HashMap::new();

View File

@@ -1,3 +1,15 @@
//! Ender chest inventory implementation.
//!
//! Ender chests are player-specific storage that persist across dimensions.
//! Each player has their own ender chest contents that is accessible from
//! any ender chest block. The inventory syncs across all ender chests
//! for that player.
//!
//! # Viewer Tracking
//!
//! Ender chests track when players open and close them to properly
//! manage the viewer count for animation purposes.
use std::{any::Any, array::from_fn, pin::Pin, sync::Arc};
use pumpkin_data::item_stack::ItemStack;
@@ -7,8 +19,17 @@ use pumpkin_world::{
};
use tokio::sync::Mutex;
/// A player's ender chest inventory.
///
/// Stores 27 slots (like a single chest) that are private to each player.
/// Contents persist across dimensions and are accessible from any
/// ender chest block.
pub struct EnderChestInventory {
/// The 27 item slots in the ender chest.
pub items: [Arc<Mutex<ItemStack>>; Self::INVENTORY_SIZE],
/// Viewer count tracker for lid animation.
///
/// Tracks how many players have the ender chest open to animate the lid.
pub tracker: Mutex<Option<Arc<ViewerCountTracker>>>,
}
@@ -19,8 +40,10 @@ impl Default for EnderChestInventory {
}
impl EnderChestInventory {
/// The size of an ender chest inventory (27 slots).
pub const INVENTORY_SIZE: usize = 27;
/// Creates a new empty ender chest inventory.
#[must_use]
pub fn new() -> Self {
Self {
@@ -29,14 +52,19 @@ impl EnderChestInventory {
}
}
/// Sets the viewer count tracker for this inventory.
///
/// Used to animate the ender chest lid based on viewers.
pub async fn set_tracker(&self, tracker: Arc<ViewerCountTracker>) {
self.tracker.lock().await.replace(tracker);
}
/// Checks if this inventory has a tracker set.
pub async fn has_tracker(&self) -> bool {
self.tracker.lock().await.is_some()
}
/// Checks if the given tracker is associated with this inventory.
pub async fn is_tracker(&self, tracker: &Arc<ViewerCountTracker>) -> bool {
if let Some(value) = self.tracker.lock().await.as_ref() {
return Arc::ptr_eq(value, tracker);

View File

@@ -1,3 +1,10 @@
//! Player inventory module.
//!
//! This module contains the player's inventory implementation, including:
//! - [`PlayerInventory`] - The 36-slot main inventory plus equipment slots
//! - [`PlayerScreenHandler`] - The screen handler for the player's inventory UI
//! - [`EnderChestInventory`] - The ender chest storage
pub mod ender_chest_inventory;
pub mod player_inventory;
pub mod player_screen_handler;

View File

@@ -1,3 +1,12 @@
//! Player inventory implementation.
//!
//! This module implements the player's inventory, which consists of:
//! - 36 main inventory slots (3 rows of 9 + hotbar)
//! - Equipment slots (armor + off-hand)
//!
//! The first 9 slots of the main inventory are the hotbar (accessible with number keys).
//! Slots 0-35 are the main inventory, with slot 40 being the off-hand slot.
use crate::entity_equipment::EntityEquipment;
use crate::screen_handler::InventoryPlayer;
@@ -5,8 +14,8 @@ use pumpkin_data::data_component_impl::EquipmentSlot;
use pumpkin_data::item_stack::ItemStack;
use pumpkin_protocol::java::client::play::CSetPlayerInventory;
use pumpkin_util::Hand;
use pumpkin_world::inventory::{Clearable, Inventory};
use pumpkin_world::inventory::{InventoryFuture, split_stack};
use pumpkin_world::inventory::split_stack;
use pumpkin_world::inventory::{Clearable, Inventory, InventoryFuture};
use std::any::Any;
use std::array::from_fn;
use std::collections::HashMap;
@@ -16,18 +25,40 @@ use std::sync::atomic::{AtomicU8, Ordering};
use tokio::sync::Mutex;
use tracing::warn;
/// The player's inventory.
///
/// Contains 36 main inventory slots (hotbar + main storage) plus
/// equipment slots accessed through [`EntityEquipment`].
pub struct PlayerInventory {
/// The 36 main inventory slots (slots 0-35).
///
/// The first 9 slots (0-8) are the hotbar, the remaining 27 (9-35) are the main storage.
pub main_inventory: [Arc<Mutex<ItemStack>>; Self::MAIN_SIZE],
/// Mapping of slot indices to equipment slot types.
///
/// Used to identify which slots correspond to armor and off-hand equipment.
pub equipment_slots: Arc<HashMap<usize, EquipmentSlot>>,
/// The currently selected hotbar slot index (0-8).
selected_slot: AtomicU8,
/// The entity equipment storage for armor and off-hand items.
///
/// This is separate from the main inventory and is rendered on the player model.
pub entity_equipment: Arc<Mutex<EntityEquipment>>,
}
impl PlayerInventory {
/// Size of the main inventory (36 slots: 27 storage + 9 hotbar).
pub const MAIN_SIZE: usize = 36;
/// Size of the hotbar (9 slots).
const HOTBAR_SIZE: usize = 9;
/// Slot index for the off-hand (40).
pub const OFF_HAND_SLOT: usize = 40;
/// Creates a new player inventory.
///
/// # Arguments
/// - `entity_equipment` - The entity equipment storage for armor/off-hand
/// - `equipment_slots` - Mapping of slot indices to equipment slots
// TODO: Add inventory load from nbt
pub fn new(
entity_equipment: Arc<Mutex<EntityEquipment>>,
@@ -42,7 +73,11 @@ impl PlayerInventory {
}
}
/// getSelectedStack in source
/// Gets the item in the currently selected hotbar slot.
///
/// This is the item the player is currently holding in their main hand.
///
/// Mojang name: `getSelectedStack`
pub fn held_item(&self) -> Arc<Mutex<ItemStack>> {
self.main_inventory
.get(self.get_selected_slot() as usize)
@@ -50,6 +85,10 @@ impl PlayerInventory {
.clone()
}
/// Gets the item in the specified hand.
///
/// # Arguments
/// - `hand` - Which hand to get the item from
pub async fn get_stack_in_hand(&self, hand: Hand) -> Arc<Mutex<ItemStack>> {
match hand {
Hand::Left => self.off_hand_item().await,
@@ -57,12 +96,18 @@ impl PlayerInventory {
}
}
/// getOffHandStack in source
/// Gets the item in the off-hand.
///
/// Mojang name: `getOffHandStack`
pub async fn off_hand_item(&self) -> Arc<Mutex<ItemStack>> {
let slot = self.equipment_slots.get(&Self::OFF_HAND_SLOT).unwrap();
self.entity_equipment.lock().await.get(slot)
}
/// Swaps the items between main hand and off-hand.
///
/// # Returns
/// The new main hand item and new off-hand item.
pub async fn swap_item(&self) -> (ItemStack, ItemStack) {
let slot = self.equipment_slots.get(&Self::OFF_HAND_SLOT).unwrap();
let mut equipment = self.entity_equipment.lock().await;
@@ -73,11 +118,13 @@ impl PlayerInventory {
(main_hand_item.clone(), off_hand_item)
}
/// Checks if a slot index is a valid hotbar slot.
#[must_use]
pub const fn is_valid_hotbar_index(slot: usize) -> bool {
slot < Self::HOTBAR_SIZE
}
/// Adds a stack to any available slot, prioritizing stacking with existing items.
async fn add_stack(&self, stack: ItemStack) -> usize {
let mut slot_index = self.get_occupied_slot_with_room_for_stack(&stack).await;
@@ -88,10 +135,13 @@ impl PlayerInventory {
if slot_index == -1 {
stack.item_count as usize
} else {
return self.add_stack_to_slot(slot_index as usize, stack).await;
self.add_stack_to_slot(slot_index as usize, stack).await
}
}
/// Adds a stack to a specific slot.
///
/// Returns the number of items that couldn't fit.
async fn add_stack_to_slot(&self, slot: usize, stack: ItemStack) -> usize {
let mut stack_count = stack.item_count;
let binding = self.get_stack(slot).await;
@@ -112,6 +162,10 @@ impl PlayerInventory {
stack_count as usize
}
/// Finds an empty slot in the inventory.
///
/// # Returns
/// The slot index or -1 if inventory is full.
async fn get_empty_slot(&self) -> i16 {
for i in 0..Self::MAIN_SIZE {
if self.main_inventory[i].lock().await.is_empty() {
@@ -122,6 +176,7 @@ impl PlayerInventory {
-1
}
/// Checks if a stack can be added to an existing stack.
fn can_stack_add_more(existing_stack: &ItemStack, stack: &ItemStack) -> bool {
!existing_stack.is_empty()
&& existing_stack.are_items_and_components_equal(stack)
@@ -129,6 +184,9 @@ impl PlayerInventory {
&& existing_stack.item_count < existing_stack.get_max_stack_size()
}
/// Finds a slot with the same item type that has room for more items.
///
/// Checks selected slot, off-hand, then other slots.
async fn get_occupied_slot_with_room_for_stack(&self, stack: &ItemStack) -> i16 {
if Self::can_stack_add_more(
&*self
@@ -155,10 +213,25 @@ impl PlayerInventory {
}
}
/// Inserts a stack into any available slot.
///
/// # Arguments
/// - `stack` - The stack to insert (modified in place)
///
/// # Returns
/// `true` if any items were inserted, `false` otherwise.
pub async fn insert_stack_anywhere(&self, stack: &mut ItemStack) -> bool {
self.insert_stack(-1, stack).await
}
/// Inserts a stack into a specific slot or any slot.
///
/// # Arguments
/// - `slot` - The slot index, or -1 for any slot
/// - `stack` - The stack to insert (modified in place)
///
/// # Returns
/// `true` if any items were inserted, `false` otherwise.
pub async fn insert_stack(&self, slot: i16, stack: &mut ItemStack) -> bool {
if stack.is_empty() {
return false;
@@ -186,6 +259,10 @@ impl PlayerInventory {
stack.item_count < i
}
/// Finds the first slot containing a matching stack.
///
/// # Returns
/// The slot index or -1 if not found.
pub async fn get_slot_with_stack(&self, stack: &ItemStack) -> i16 {
for i in 0..Self::MAIN_SIZE {
if !self.main_inventory[i].lock().await.is_empty()
@@ -201,7 +278,10 @@ impl PlayerInventory {
-1
}
pub async fn get_swappable_hotbar_slot(&self) -> usize {
/// Finds an empty hotbar slot to swap an item to.
///
/// First looks for empty slots, then slots without enchantments.
async fn get_swappable_hotbar_slot(&self) -> usize {
let selected_slot = self.get_selected_slot() as usize;
for i in 0..Self::HOTBAR_SIZE {
let check_index = (i + selected_slot) % 9;
@@ -210,18 +290,17 @@ impl PlayerInventory {
}
}
for i in 0..Self::HOTBAR_SIZE {
if let Some(i) = (0..Self::HOTBAR_SIZE).next() {
let check_index = (i + selected_slot) % 9;
if true
/*TODO: If item has an enchantment skip it */
{
return check_index;
}
return check_index;
}
self.get_selected_slot() as usize
}
/// Swaps an item stack with an item on the hotbar.
///
/// Finds an empty hotbar slot and places the stack there.
pub async fn swap_stack_with_hotbar(&self, stack: ItemStack) {
self.set_selected_slot(self.get_swappable_hotbar_slot().await as u8);
@@ -247,6 +326,7 @@ impl PlayerInventory {
.await;
}
/// Swaps the items at two slot indices.
pub async fn swap_slot_with_hotbar(&self, slot: usize) {
self.set_selected_slot(self.get_swappable_hotbar_slot().await as u8);
let stack = self.main_inventory[self.get_selected_slot() as usize]
@@ -261,10 +341,17 @@ impl PlayerInventory {
self.set_stack(slot, stack).await;
}
/// Gives a stack to the player or drops it if inventory is full.
pub async fn offer_or_drop_stack(&self, stack: ItemStack, player: &dyn InventoryPlayer) {
self.offer(stack, true, player).await;
}
/// Gives a stack to the player, optionally notifying the client.
///
/// # Arguments
/// - `stack` - The stack to give
/// - `notify_client` - Whether to send inventory update packets
/// - `player` - The player to give the stack to
pub async fn offer(&self, stack: ItemStack, notify_client: bool, player: &dyn InventoryPlayer) {
let mut stack = stack;
while !stack.is_empty() {
@@ -361,6 +448,24 @@ impl Inventory for PlayerInventory {
})
}
fn remove_stack(&self, slot: usize) -> InventoryFuture<'_, ItemStack> {
Box::pin(async move {
if slot < self.main_inventory.len() {
let mut removed = ItemStack::EMPTY.clone();
let mut guard = self.main_inventory[slot].lock().await;
std::mem::swap(&mut removed, &mut *guard);
removed
} else {
let slot = self.equipment_slots.get(&slot).unwrap();
self.entity_equipment
.lock()
.await
.put(slot, ItemStack::EMPTY.clone())
.await
}
})
}
fn remove_stack_specific(&self, slot: usize, amount: u8) -> InventoryFuture<'_, ItemStack> {
Box::pin(async move {
if slot < self.main_inventory.len() {
@@ -380,24 +485,6 @@ impl Inventory for PlayerInventory {
})
}
fn remove_stack(&self, slot: usize) -> InventoryFuture<'_, ItemStack> {
Box::pin(async move {
if slot < self.main_inventory.len() {
let mut removed = ItemStack::EMPTY.clone();
let mut guard = self.main_inventory[slot].lock().await;
std::mem::swap(&mut removed, &mut *guard);
removed
} else {
let slot = self.equipment_slots.get(&slot).unwrap();
self.entity_equipment
.lock()
.await
.put(slot, ItemStack::EMPTY.clone())
.await
}
})
}
fn set_stack(&self, slot: usize, stack: ItemStack) -> InventoryFuture<'_, ()> {
Box::pin(async move {
if slot < self.main_inventory.len() {
@@ -418,6 +505,10 @@ impl Inventory for PlayerInventory {
}
impl PlayerInventory {
/// Sets the selected hotbar slot.
///
/// # Panics
/// Panics if the slot index is not a valid hotbar index.
pub fn set_selected_slot(&self, slot: u8) {
if Self::is_valid_hotbar_index(slot as usize) {
self.selected_slot.store(slot, Ordering::Relaxed);
@@ -426,6 +517,7 @@ impl PlayerInventory {
}
}
/// Gets the currently selected hotbar slot index.
pub fn get_selected_slot(&self) -> u8 {
self.selected_slot.load(Ordering::Relaxed)
}

View File

@@ -1,3 +1,23 @@
//! Player inventory screen handler.
//!
//! This module handles the player's inventory screen (opened with E key).
//! It includes:
//! - The 2x2 crafting grid (inventory crafting)
//! - Armor slots (head, chest, legs, feet)
//! - Main inventory (27 slots)
//! - Hotbar (9 slots)
//! - Offhand slot
//!
//! # Slot Layout
//!
//! The player screen handler uses the following slot indices:
//! - 0: Crafting result
//! - 1-4: Crafting grid (2x2)
//! - 5-8: Armor slots (head, chest, legs, feet)
//! - 9-35: Main inventory
//! - 36-44: Hotbar
//! - 45: Offhand
use super::player_inventory::PlayerInventory;
use crate::crafting::crafting_inventory::CraftingInventory;
use crate::crafting::crafting_screen_handler::CraftingScreenHandler;
@@ -13,8 +33,14 @@ use pumpkin_world::inventory::Inventory;
use std::any::Any;
use std::sync::Arc;
/// Screen handler for the player's inventory.
///
/// Manages the player's inventory UI including crafting, armor, and
/// the main inventory. This is the default screen shown when pressing E.
pub struct PlayerScreenHandler {
/// Core screen handler behavior (slots, sync ID, listeners).
behaviour: ScreenHandlerBehaviour,
/// The 2x2 crafting grid inventory.
crafting_inventory: Arc<dyn RecipeInputInventory>,
}
@@ -24,6 +50,7 @@ impl CraftingScreenHandler<CraftingInventory> for PlayerScreenHandler {}
// TODO: Fully implement this
impl PlayerScreenHandler {
/// Equipment slot order for armor display.
const EQUIPMENT_SLOT_ORDER: [EquipmentSlot; 4] = [
EquipmentSlot::HEAD,
EquipmentSlot::CHEST,
@@ -31,15 +58,25 @@ impl PlayerScreenHandler {
EquipmentSlot::FEET,
];
/// Checks if a slot index is in the hotbar.
///
/// Hotbar slots are 36-44 in the protocol (0-indexed 36-44).
#[must_use]
pub fn is_in_hotbar(slot: u8) -> bool {
(36..=45).contains(&slot)
}
/// Gets a slot by its index.
pub fn get_slot(&self, slot: usize) -> Arc<dyn Slot> {
self.behaviour.slots[slot].clone()
}
/// Creates a new player screen handler.
///
/// # Arguments
/// - `player_inventory` - The player's inventory
/// - `window_type` - The window type (usually None for player inventory)
/// - `sync_id` - The synchronization ID
pub async fn new(
player_inventory: &Arc<PlayerInventory>,
window_type: Option<WindowType>,
@@ -57,6 +94,7 @@ impl PlayerScreenHandler {
.add_recipe_slots(crafting_inventory)
.await;
// Add armor slots (head, chest, legs, feet)
for i in 0..4 {
player_screen_handler.add_slot(Arc::new(ArmorSlot::new(
player_inventory.clone(),
@@ -67,10 +105,11 @@ impl PlayerScreenHandler {
let player_inventory: Arc<dyn Inventory> = player_inventory.clone();
// Add main inventory and hotbar
player_screen_handler.add_player_slots(&player_inventory);
// Offhand
// TODO: public void setStack(ItemStack stack, ItemStack previousStack) { owner.onEquipStack(EquipmentSlot.OFFHAND, previousStack, stack);
// Offhand slot (index 40 in player inventory, 45 in screen handler)
// TODO: onEquipStack callback for offhand
player_screen_handler.add_slot(Arc::new(NormalSlot::new(player_inventory.clone(), 40)));
player_screen_handler
@@ -78,15 +117,6 @@ impl PlayerScreenHandler {
}
impl ScreenHandler for PlayerScreenHandler {
fn on_closed<'a>(&'a mut self, player: &'a dyn InventoryPlayer) -> ScreenHandlerFuture<'a, ()> {
Box::pin(async move {
self.default_on_closed(player).await;
//TODO: this.craftingResultInventory.clear();
self.drop_inventory(player, self.crafting_inventory.clone())
.await;
})
}
fn as_any(&self) -> &dyn Any {
self
}
@@ -99,9 +129,25 @@ impl ScreenHandler for PlayerScreenHandler {
&mut self.behaviour
}
/// Do quick move (Shift + Click) for the given slot index.
fn on_closed<'a>(&'a mut self, player: &'a dyn InventoryPlayer) -> ScreenHandlerFuture<'a, ()> {
Box::pin(async move {
self.default_on_closed(player).await;
//TODO: this.craftingResultInventory.clear();
self.drop_inventory(player, self.crafting_inventory.clone())
.await;
})
}
/// Performs quick move (shift-click) for the given slot.
///
/// Returns the moved stack if successful, or `ItemStack::EMPTY` if nothing changed.
/// The quick move logic depends on the source slot:
/// - Crafting result (0) -> Player inventory (from end)
/// - Crafting grid (1-4) -> Player inventory (from start)
/// - Armor slots (5-8) -> Player inventory, unequips
/// - Armor items -> Armor slots if empty
/// - Offhand items -> Offhand slot if empty
/// - Main inventory (9-35) -> Hotbar
/// - Hotbar (36-44) -> Main inventory
fn quick_move<'a>(
&'a mut self,
player: &'a dyn InventoryPlayer,
@@ -121,7 +167,7 @@ impl ScreenHandler for PlayerScreenHandler {
.get_data_component::<EquippableImpl>()
.map_or(&EquipmentSlot::MAIN_HAND, |equippable| equippable.slot);
// Quick move logic
// Quick move logic based on source slot
let success = if slot_index == 0 {
// From crafting result slot (0) -> Player Inventory (9-45, from end)
self.insert_item(&mut slot_stack, 9, 45, true).await

View File

@@ -1,3 +1,30 @@
//! Screen handler module.
//!
//! This module defines the core screen handler system for container UIs.
//! A screen handler manages the server-side state of a container interface,
//! handling slot layout, click processing, item transfer, and synchronization
//! with the client.
//!
//! # Core Components
//!
//! - [`ScreenHandler`] - The main trait for container screen handlers
//! - [`ScreenHandlerBehaviour`] - Shared state for all screen handlers
//! - [`InventoryPlayer`] - Interface for player interactions with containers
//! - [`ScreenProperty`] - Container UI properties (progress bars, etc.)
//!
//! # Screen Handler Lifecycle
//!
//! 1. Creation - Screen handler is created with slots and sync ID
//! 2. Opening - Player opens the container, sync handler attaches
//! 3. Interaction - Click packets are processed, items move between slots
//! 4. Closing - Container closes, cursor item is dropped/given to player
//!
//! # Slot Indexing
//!
//! Slots are indexed from 0 within each screen handler. Special values:
//! - `-1` - Cursor slot (held item)
//! - `-999` - Outside inventory (drop to world)
use crate::{
container_click::MouseClick,
player::player_inventory::PlayerInventory,
@@ -30,8 +57,13 @@ use std::{cmp::max, pin::Pin};
use tokio::sync::Mutex;
use tracing::warn;
/// Slot index indicating a click outside the inventory.
const SLOT_INDEX_OUTSIDE: i32 = -999;
/// A tracked property for container UI elements.
///
/// Properties are used to synchronize UI state like furnace progress bars,
/// enchantment levels, and other visual indicators between server and client.
pub struct ScreenProperty {
old_value: i32,
index: u8,
@@ -39,6 +71,11 @@ pub struct ScreenProperty {
}
impl ScreenProperty {
/// Creates a new screen property.
///
/// # Arguments
/// - `value` - The property delegate that holds the actual value
/// - `index` - The property index for multi-value delegates
pub fn new(value: Arc<dyn PropertyDelegate>, index: u8) -> Self {
Self {
old_value: value.get_property(i32::from(index)),
@@ -47,15 +84,20 @@ impl ScreenProperty {
}
}
/// Gets the current property value.
#[must_use]
pub fn get(&self) -> i32 {
self.value.get_property(i32::from(self.index))
}
/// Sets the property value.
pub fn set(&mut self, value: i32) {
self.value.set_property(i32::from(self.index), value);
}
/// Checks if the value has changed since the last check.
///
/// Updates the old value to the current value.
pub fn has_changed(&mut self) -> bool {
let value = self.get();
let has_changed = !value.eq(&self.old_value);
@@ -64,31 +106,64 @@ impl ScreenProperty {
}
}
/// Type alias for async player operations.
/// Type alias for async player operations.
pub type PlayerFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
/// Interface for player interactions with containers.
///
/// This trait abstracts the player's ability to:
/// - Drop items into the world
/// - Receive inventory packets
/// - Change equipment
/// - Receive experience
///
/// Implementors are typically player entities that can open containers.
pub trait InventoryPlayer: Send + Sync {
/// Drops an item into the world.
///
/// # Arguments
/// - `item` - The item to drop
/// - `retain_ownership` - If true, the player keeps ownership (for pickup delay)
fn drop_item(&self, item: ItemStack, retain_ownership: bool) -> PlayerFuture<'_, ()>;
/// Gets the player's inventory.
fn get_inventory(&self) -> Arc<PlayerInventory>;
/// Checks if the player has infinite materials (creative mode).
fn has_infinite_materials(&self) -> bool;
/// Sends a full container content packet.
fn enqueue_inventory_packet<'a>(
&'a self,
packet: &'a CSetContainerContent,
) -> PlayerFuture<'a, ()>;
/// Sends a single slot update packet.
fn enqueue_slot_packet<'a>(&'a self, packet: &'a CSetContainerSlot) -> PlayerFuture<'a, ()>;
/// Sends a cursor item update packet.
fn enqueue_cursor_packet<'a>(&'a self, packet: &'a CSetCursorItem) -> PlayerFuture<'a, ()>;
/// Sends a property update packet.
fn enqueue_property_packet<'a>(
&'a self,
packet: &'a CSetContainerProperty,
) -> PlayerFuture<'a, ()>;
/// Sends a player inventory slot update.
fn enqueue_slot_set_packet<'a>(
&'a self,
packet: &'a CSetPlayerInventory,
) -> PlayerFuture<'a, ()>;
/// Sends a selected slot update.
fn enqueue_set_held_item_packet<'a>(
&'a self,
packet: &'a CSetSelectedSlot,
) -> PlayerFuture<'a, ()>;
/// Sends an equipment change packet.
fn enqueue_equipment_change<'a>(
&'a self,
slot: &'a EquipmentSlot,
@@ -99,6 +174,10 @@ pub trait InventoryPlayer: Send + Sync {
fn award_experience(&self, amount: i32) -> PlayerFuture<'_, ()>;
}
/// Gives a stack to the player or drops it if inventory is full.
///
/// Tries to insert the stack into the player's inventory first,
/// and drops it in the world if there's no room.
pub async fn offer_or_drop_stack(player: &dyn InventoryPlayer, stack: ItemStack) {
// TODO: Super weird disconnect logic in vanilla, investigate this later
player
@@ -107,37 +186,62 @@ pub async fn offer_or_drop_stack(player: &dyn InventoryPlayer, stack: ItemStack)
.await;
}
/// Type alias for async screen handler operations.
pub type ScreenHandlerFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
// Future returning ItemStack (used by quick_move)
/// Future type that returns an `ItemStack` (used by `quick_move`).
pub type ItemStackFuture<'a> = ScreenHandlerFuture<'a, ItemStack>;
// Future returning Option<usize>
/// Future type that returns an optional slot index.
pub type OptionUsizeFuture<'a> = ScreenHandlerFuture<'a, Option<usize>>;
//ScreenHandler.java
/// The main trait for container screen handlers.
///
/// Screen handlers manage the server-side state of container UIs like chests,
/// furnaces, crafting tables, etc. They handle:
/// - Slot layout and management
/// - Click processing
/// - Item transfer logic (shift-click)
/// - Client synchronization
///
/// # Implementation
///
/// Implementors must provide:
/// - [`get_behaviour`](ScreenHandler::get_behaviour) and [`get_behaviour_mut`](ScreenHandler::get_behaviour_mut)
/// - [`quick_move`](ScreenHandler::quick_move) for shift-click behavior
/// - [`as_any`](ScreenHandler::as_any) for downcasting
// ScreenHandler.java
// TODO: Fully implement this
pub trait ScreenHandler: Send + Sync {
// --- Synchronous Methods (Unchanged) ---
// --- Synchronous Methods ---
/// Gets the window type for this screen handler.
fn window_type(&self) -> Option<WindowType> {
self.get_behaviour().window_type
}
/// Returns this screen handler as an Any reference.
fn as_any(&self) -> &dyn Any;
/// Gets the sync ID for this screen handler.
fn sync_id(&self) -> u8 {
self.get_behaviour().sync_id
}
/// Checks if the player can use this container.
fn can_use(&self, _player: &dyn InventoryPlayer) -> bool {
true
}
/// Gets a reference to the screen handler behaviour.
fn get_behaviour(&self) -> &ScreenHandlerBehaviour;
/// Gets a mutable reference to the screen handler behaviour.
fn get_behaviour_mut(&mut self) -> &mut ScreenHandlerBehaviour;
/// Adds a slot to this screen handler.
///
/// Assigns an ID and sets up tracking for the slot.
fn add_slot(&mut self, slot: Arc<dyn Slot>) -> Arc<dyn Slot> {
let behaviour = self.get_behaviour_mut();
slot.set_id(behaviour.slots.len());
@@ -148,12 +252,14 @@ pub trait ScreenHandler: Send + Sync {
slot
}
/// Adds hotbar slots (0-8) from the player inventory.
fn add_player_hotbar_slots(&mut self, player_inventory: &Arc<dyn Inventory>) {
for i in 0..9 {
self.add_slot(Arc::new(NormalSlot::new(player_inventory.clone(), i)));
}
}
/// Adds main inventory slots (9-35) from the player inventory.
fn add_player_inventory_slots(&mut self, player_inventory: &Arc<dyn Inventory>) {
for i in 0..3 {
for j in 0..9 {
@@ -165,11 +271,13 @@ pub trait ScreenHandler: Send + Sync {
}
}
/// Adds all player inventory slots (main + hotbar).
fn add_player_slots(&mut self, player_inventory: &Arc<dyn Inventory>) {
self.add_player_inventory_slots(player_inventory);
self.add_player_hotbar_slots(player_inventory);
}
/// Records a received hash for a slot (for sync tracking).
fn set_received_hash(&mut self, slot: usize, hash: OptionalItemStackHash) {
let behaviour = self.get_behaviour_mut();
if slot < behaviour.previous_tracked_stacks.len() {
@@ -183,36 +291,44 @@ pub trait ScreenHandler: Send + Sync {
}
}
/// Records a received stack for a slot (for sync tracking).
fn set_received_stack(&mut self, slot: usize, stack: ItemStack) {
let behaviour = self.get_behaviour_mut();
behaviour.previous_tracked_stacks[slot].set_received_stack(stack);
}
/// Records a received cursor hash (for sync tracking).
fn set_received_cursor_hash(&mut self, hash: OptionalItemStackHash) {
let behaviour = self.get_behaviour_mut();
behaviour.previous_cursor_stack.set_received_hash(hash);
}
/// Adds a property to track.
fn add_property(&mut self, property: ScreenProperty) {
let behaviour = self.get_behaviour_mut();
behaviour.properties.push(property);
behaviour.tracked_property_values.push(0);
}
/// Adds multiple properties to track.
fn add_properties(&mut self, properties: Vec<ScreenProperty>) {
for property in properties {
self.add_property(property);
}
}
// --- Asynchronous Methods (Refactored) ---
// --- Asynchronous Methods ---
/// Called when the container is closed by the player.
///
/// Default implementation drops the cursor item.
fn on_closed<'a>(&'a mut self, player: &'a dyn InventoryPlayer) -> ScreenHandlerFuture<'a, ()> {
Box::pin(async move {
self.default_on_closed(player).await;
})
}
/// Default close behavior - drops the cursor item.
fn default_on_closed<'a>(
&'a mut self,
player: &'a dyn InventoryPlayer,
@@ -230,6 +346,7 @@ pub trait ScreenHandler: Send + Sync {
})
}
/// Drops all items from an inventory into the world.
fn drop_inventory<'a>(
&'a self,
player: &'a dyn InventoryPlayer,
@@ -242,6 +359,9 @@ pub trait ScreenHandler: Send + Sync {
})
}
/// Copies tracked slot state from another screen handler.
///
/// Used when reopening a container to restore previous state.
fn copy_shared_slots(
&mut self,
other: Arc<Mutex<dyn ScreenHandler>>,
@@ -278,6 +398,9 @@ pub trait ScreenHandler: Send + Sync {
})
}
/// Synchronizes the full state to the client.
///
/// Captures current slot states and sends a full update packet.
fn sync_state(&mut self) -> ScreenHandlerFuture<'_, ()> {
Box::pin(async move {
let behaviour = self.get_behaviour_mut();
@@ -315,6 +438,7 @@ pub trait ScreenHandler: Send + Sync {
})
}
/// Adds a listener for slot and property changes.
fn add_listener(
&mut self,
listener: Arc<dyn ScreenHandlerListener>,
@@ -325,6 +449,7 @@ pub trait ScreenHandler: Send + Sync {
})
}
/// Attaches a sync handler and performs initial sync.
fn update_sync_handler(
&mut self,
sync_handler: Arc<SyncHandler>,
@@ -336,6 +461,9 @@ pub trait ScreenHandler: Send + Sync {
})
}
/// Sends all updates to the client.
///
/// Updates tracked slots and properties.
fn update_to_client(&mut self) -> ScreenHandlerFuture<'_, ()> {
Box::pin(async move {
for i in 0..self.get_behaviour().slots.len() {
@@ -363,6 +491,7 @@ pub trait ScreenHandler: Send + Sync {
})
}
/// Updates a tracked property value.
fn update_tracked_properties(&mut self, idx: i32, value: i32) -> ScreenHandlerFuture<'_, ()> {
Box::pin(async move {
let behaviour = self.get_behaviour_mut();
@@ -377,6 +506,7 @@ pub trait ScreenHandler: Send + Sync {
})
}
/// Checks if a property needs to be synced to the client.
fn check_property_updates(&mut self, idx: i32, value: i32) -> ScreenHandlerFuture<'_, ()> {
Box::pin(async move {
let behaviour = self.get_behaviour_mut();
@@ -396,6 +526,7 @@ pub trait ScreenHandler: Send + Sync {
})
}
/// Updates the tracked state of a slot.
fn update_tracked_slot(
&mut self,
slot: usize,
@@ -416,6 +547,7 @@ pub trait ScreenHandler: Send + Sync {
})
}
/// Checks if a slot needs to be synced to the client.
fn check_slot_updates(&mut self, slot: usize, stack: ItemStack) -> ScreenHandlerFuture<'_, ()> {
Box::pin(async move {
let behaviour = self.get_behaviour_mut();
@@ -435,6 +567,7 @@ pub trait ScreenHandler: Send + Sync {
})
}
/// Checks if the cursor stack needs to be synced.
fn check_cursor_stack_updates(&mut self) -> ScreenHandlerFuture<'_, ()> {
Box::pin(async move {
let behaviour = self.get_behaviour_mut();
@@ -454,6 +587,7 @@ pub trait ScreenHandler: Send + Sync {
})
}
/// Sends all content updates to listeners and sync handler.
fn send_content_updates(&mut self) -> ScreenHandlerFuture<'_, ()> {
Box::pin(async move {
let slots_len = self.get_behaviour().slots.len();
@@ -484,22 +618,26 @@ pub trait ScreenHandler: Send + Sync {
})
}
/// Checks if a slot index is valid.
fn is_slot_valid(&self, slot: i32) -> ScreenHandlerFuture<'_, bool> {
Box::pin(async move {
slot == -1 || slot == -999 || slot < self.get_behaviour().slots.len() as i32
})
}
/// Disables synchronization (for batch operations).
fn disable_sync(&mut self) {
let behaviour = self.get_behaviour_mut();
behaviour.disable_sync = true;
}
/// Re-enables synchronization.
fn enable_sync(&mut self) {
let behaviour = self.get_behaviour_mut();
behaviour.disable_sync = false;
}
/// Gets the screen handler slot index for an inventory slot.
fn get_slot_index<'a>(
&'a self,
inventory: &'a Arc<dyn Inventory>,
@@ -513,12 +651,19 @@ pub trait ScreenHandler: Send + Sync {
})
}
/// Performs a quick move (shift-click) from a slot.
///
/// Must be implemented by concrete screen handlers to define
/// where items go when shift-clicked from specific slots.
fn quick_move<'a>(
&'a mut self,
player: &'a dyn InventoryPlayer,
slot_index: i32,
) -> ItemStackFuture<'a>;
/// Inserts an item into a range of slots.
///
/// First tries to stack with existing items, then fills empty slots.
fn insert_item<'a>(
&'a mut self,
stack: &'a mut ItemStack,
@@ -610,6 +755,9 @@ pub trait ScreenHandler: Send + Sync {
})
}
/// Handles a slot click event.
///
/// Override for custom click handling. Return true to prevent default handling.
fn handle_slot_click<'a>(
&'a self,
_player: &'a dyn InventoryPlayer,
@@ -624,6 +772,7 @@ pub trait ScreenHandler: Send + Sync {
})
}
/// Public entry point for slot click handling.
fn on_slot_click<'a>(
&'a mut self,
slot_index: i32,
@@ -637,6 +786,9 @@ pub trait ScreenHandler: Send + Sync {
})
}
/// Internal slot click handling implementation.
///
/// Handles all click types: pickup, quick move, swap, throw, drag, clone.
#[expect(clippy::too_many_lines)]
fn internal_on_slot_click<'a>(
&'a mut self,
@@ -1053,20 +1205,34 @@ pub trait ScreenHandlerFactory: Send + Sync {
}
pub struct ScreenHandlerBehaviour {
/// Slots in this screen handler (includes both container and player slots).
pub slots: Vec<Arc<dyn Slot>>,
/// Sync ID for client-server matching (matches the window ID in protocol).
pub sync_id: u8,
/// Registered listeners for slot/property changes.
pub listeners: Vec<Arc<dyn ScreenHandlerListener>>,
/// Sync handler for sending updates to the client.
pub sync_handler: Option<Arc<SyncHandler>>,
/// Current tracked stacks for comparison with previous state.
//TODO: Check if this is needed
pub tracked_stacks: Vec<ItemStack>,
/// The item currently held by the player's cursor (held item).
pub cursor_stack: Arc<Mutex<ItemStack>>,
/// Previous tracked stacks for detecting changes that need syncing.
pub previous_tracked_stacks: Vec<TrackedStack>,
/// Previous cursor stack for detecting cursor changes.
pub previous_cursor_stack: TrackedStack,
/// Revision counter for sync tracking (increments on each change).
pub revision: AtomicU32,
/// Whether sync is temporarily disabled (for batch operations).
pub disable_sync: bool,
/// Container properties (furnace progress, enchantment levels, etc.).
pub properties: Vec<ScreenProperty>,
/// Tracked property values for detecting changes.
pub tracked_property_values: Vec<i32>,
/// The window type for this container ( determines client UI).
pub window_type: Option<WindowType>,
/// Slots selected during a drag operation (for multi-slot distribution).
pub drag_slots: Vec<u32>,
}

View File

@@ -1,3 +1,23 @@
//! Inventory slot implementations.
//!
//! This module defines the [`Slot`] trait and its implementations. Slots represent
//! individual positions in an inventory that can hold items.
//!
//! # Slot Types
//!
//! - [`NormalSlot`] - A basic inventory slot with no restrictions
//! - [`ArmorSlot`] - An armor slot that only accepts appropriate item types
//! (helmets in head slot, chestplates in chest slot, etc.)
//!
//! # Slot Operations
//!
//! Slots support various operations:
//! - Getting/setting the item stack
//! - Checking if items can be inserted
//! - Taking items from the slot
//! - Marking the slot as changed (dirty)
//! - Callbacks for slot interaction events
use std::{
pin::Pin,
sync::{
@@ -15,22 +35,35 @@ use pumpkin_data::item_stack::ItemStack;
use pumpkin_world::inventory::Inventory;
use tokio::{sync::Mutex, time::timeout};
/// Type alias for async slot operations.
pub type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
/// A slot in an inventory.
///
/// The slot trait defines how individual inventory positions behave.
/// Different slot types (normal, armor, result slots) implement this
/// trait to enforce their specific restrictions.
// Slot.java
// This is a trait due to crafting slots being a thing
pub trait Slot: Send + Sync {
/// Returns the inventory containing this slot.
fn get_inventory(&self) -> Arc<dyn Inventory>;
/// Returns the index of this slot within its inventory.
fn get_index(&self) -> usize;
/// Sets the protocol ID of this slot.
fn set_id(&self, index: usize);
/// Used to notify result slots that they need to update their contents. (e.g. refill)
/// Note that you **MUST** call this after changing the stack in the slot, and releasing any
/// locks to the stack to avoid deadlocks.
/// Callback for when an item is quick-moved from this slot.
///
/// Also see: `ScreenHandler::quick_move`
/// Used to notify result slots (like crafting output) that they
/// need to refill their contents.
///
/// # Note
/// You **MUST** call this after changing the stack and releasing
/// any locks to avoid deadlocks.
///
/// Also see: [`ScreenHandler::quick_move`](crate::screen_handler::ScreenHandler::quick_move)
fn on_quick_move_crafted(
&self,
_stack: ItemStack,
@@ -39,9 +72,9 @@ pub trait Slot: Send + Sync {
Box::pin(async {}) // Default implementation
}
/// Callback for when an item is taken from the slot.
/// Callback for when an item is taken from this slot.
///
/// Also see: `safe_take`
/// Also see: [`safe_take`]
fn on_take_item<'a>(
&'a self,
_player: &'a dyn InventoryPlayer,
@@ -53,21 +86,29 @@ pub trait Slot: Send + Sync {
})
}
// Used for plugins
/// Plugin callback for slot clicks.
///
/// Called when a player clicks on this slot. Can be used by
/// plugins to intercept or modify click behavior.
fn on_click(&self, _player: &dyn InventoryPlayer) -> BoxFuture<'_, ()> {
Box::pin(async {}) // Default implementation
}
/// Checks if the given stack can be inserted into this slot.
fn can_insert<'a>(&'a self, _stack: &'a ItemStack) -> BoxFuture<'a, bool> {
// Default implementation logic:
Box::pin(async move { true })
}
/// Gets the stack in this slot.
fn get_stack(&self) -> BoxFuture<'_, Arc<Mutex<ItemStack>>> {
// Default implementation logic:
Box::pin(async move { self.get_inventory().get_stack(self.get_index()).await })
}
/// Gets a copy of the stack in this slot.
///
/// Acquires a lock and returns a clone of the stack.
fn get_cloned_stack(&self) -> BoxFuture<'_, ItemStack> {
// Default implementation logic:
Box::pin(async move {
@@ -80,6 +121,7 @@ pub trait Slot: Send + Sync {
})
}
/// Checks if this slot has a non-empty stack.
fn has_stack(&self) -> BoxFuture<'_, bool> {
// Default implementation logic:
Box::pin(async move {
@@ -92,7 +134,10 @@ pub trait Slot: Send + Sync {
})
}
/// Make sure to drop any locks to the slot stack before calling this
/// Sets the stack in this slot.
///
/// # Note
/// Make sure to drop any locks to the slot stack before calling this.
fn set_stack(&self, stack: ItemStack) -> BoxFuture<'_, ()> {
// Default implementation logic:
Box::pin(async move {
@@ -100,7 +145,9 @@ pub trait Slot: Send + Sync {
})
}
/// Changes the stack in the slot with the given `stack`.
/// Sets the stack with previous stack reference.
///
/// Some slots (like armor) need to know the previous stack for callbacks.
fn set_stack_prev(&self, stack: ItemStack, _previous_stack: ItemStack) -> BoxFuture<'_, ()> {
// Default implementation logic:
Box::pin(async move {
@@ -108,6 +155,7 @@ pub trait Slot: Send + Sync {
})
}
/// Sets the stack without calling callbacks.
fn set_stack_no_callbacks(&self, stack: ItemStack) -> BoxFuture<'_, ()> {
// Default implementation logic:
Box::pin(async move {
@@ -117,13 +165,18 @@ pub trait Slot: Send + Sync {
})
}
fn mark_dirty(&self) -> BoxFuture<'_, ()>; // This method must be implemented by concrete types
/// Marks this slot as changed.
///
/// Must be implemented by concrete types.
fn mark_dirty(&self) -> BoxFuture<'_, ()>;
/// Gets the maximum item count for this slot.
fn get_max_item_count(&self) -> BoxFuture<'_, u8> {
// Default implementation logic:
Box::pin(async move { self.get_inventory().get_max_count_per_stack() })
}
/// Gets the maximum item count for the given stack in this slot.
fn get_max_item_count_for_stack<'a>(&'a self, stack: &'a ItemStack) -> BoxFuture<'a, u8> {
// Default implementation logic:
Box::pin(async move {
@@ -133,7 +186,7 @@ pub trait Slot: Send + Sync {
})
}
/// Removes a specific amount of items from the slot.
/// Removes a specific amount of items from this slot.
///
/// Mojang name: `remove`
fn take_stack(&self, amount: u8) -> BoxFuture<'_, ItemStack> {
@@ -144,12 +197,16 @@ pub trait Slot: Send + Sync {
})
}
/// Checks if the player can take items from this slot.
///
/// Mojang name: `mayPickup`
fn can_take_items(&self, _player: &dyn InventoryPlayer) -> BoxFuture<'_, bool> {
// Default implementation logic:
Box::pin(async move { true })
}
/// Checks if this slot can be modified by the player.
///
/// Mojang name: `allowModification`
fn allow_modification<'a>(&'a self, player: &'a dyn InventoryPlayer) -> BoxFuture<'a, bool> {
// Default implementation logic:
@@ -159,6 +216,11 @@ pub trait Slot: Send + Sync {
})
}
/// Tries to take a stack in the given range.
///
/// Returns `None` if can't take items or if slot is empty.
/// For result slots, cannot take partial stacks.
///
/// Mojang name: `tryRemove`
fn try_take_stack_range<'a>(
&'a self,
@@ -193,8 +255,9 @@ pub trait Slot: Send + Sync {
})
}
/// Safely tries to take a stack of items from the slot, returning `None` if the stack is empty.
/// Considering such as result slots, as their stacks cannot split.
/// Safely tries to take a stack of items from the slot.
///
/// Returns an empty stack if can't take. Triggers callbacks.
///
/// Mojang name: `safeTake`
fn safe_take<'a>(
@@ -214,6 +277,9 @@ pub trait Slot: Send + Sync {
})
}
/// Inserts a stack into this slot.
///
/// Returns any leftover items that couldn't fit.
fn insert_stack(&self, stack: ItemStack) -> BoxFuture<'_, ItemStack> {
// Default implementation logic:
Box::pin(async move {
@@ -222,6 +288,9 @@ pub trait Slot: Send + Sync {
})
}
/// Inserts a specific count from a stack.
///
/// Returns any leftover items.
fn insert_stack_count(&self, mut stack: ItemStack, count: u8) -> BoxFuture<'_, ItemStack> {
// Default implementation logic:
Box::pin(async move {
@@ -254,14 +323,25 @@ pub trait Slot: Send + Sync {
}
}
/// Just called Slot in Vanilla
/// A normal inventory slot.
///
/// Just called `Slot` in vanilla Minecraft. This is the basic
/// slot implementation with no special restrictions.
pub struct NormalSlot {
/// The inventory containing this slot.
pub inventory: Arc<dyn Inventory>,
/// Index of this slot within its inventory.
pub index: usize,
/// Protocol ID for this slot (assigned by screen handler).
pub id: AtomicU8,
}
impl NormalSlot {
/// Creates a new normal slot.
///
/// # Arguments
/// - `inventory` - The containing inventory
/// - `index` - The slot index within the inventory
pub fn new(inventory: Arc<dyn Inventory>, index: usize) -> Self {
Self {
inventory,
@@ -270,6 +350,7 @@ impl NormalSlot {
}
}
}
impl Slot for NormalSlot {
fn get_inventory(&self) -> Arc<dyn Inventory> {
self.inventory.clone()
@@ -290,15 +371,32 @@ impl Slot for NormalSlot {
}
}
/// An armor equipment slot.
///
/// Restricts which items can be placed based on the equipment slot type:
/// - Head: Helmets, skulls, carved pumpkins
/// - Chest: Chestplates, elytra
/// - Legs: Leggings
/// - Feet: Boots
// ArmorSlot.java
pub struct ArmorSlot {
/// The inventory containing this slot (usually player inventory).
pub inventory: Arc<dyn Inventory>,
/// Index of this slot within its inventory.
pub index: usize,
/// Protocol ID for this slot (assigned by screen handler).
pub id: AtomicU8,
/// The equipment slot type (head, chest, legs, feet, or off-hand).
pub equipment_slot: EquipmentSlot,
}
impl ArmorSlot {
/// Creates a new armor slot.
///
/// # Arguments
/// - `inventory` - The containing inventory
/// - `index` - The slot index
/// - `equipment_slot` - The equipment slot type (head, chest, legs, feet)
pub fn new(inventory: Arc<dyn Inventory>, index: usize, equipment_slot: EquipmentSlot) -> Self {
Self {
inventory,
@@ -322,17 +420,7 @@ impl Slot for ArmorSlot {
self.id.store(id as u8, Ordering::Relaxed);
}
fn get_max_item_count(&self) -> BoxFuture<'_, u8> {
Box::pin(async move { 1 })
}
fn set_stack_prev(&self, stack: ItemStack, _previous_stack: ItemStack) -> BoxFuture<'_, ()> {
Box::pin(async move {
//TODO: this.entity.onEquipStack(this.equipmentSlot, previousStack, stack);
self.set_stack_no_callbacks(stack).await;
})
}
/// Restricts inserts to appropriate armor types.
fn can_insert<'a>(&'a self, stack: &'a ItemStack) -> BoxFuture<'a, bool> {
Box::pin(async move {
match self.equipment_slot {
@@ -347,10 +435,10 @@ impl Slot for ArmorSlot {
})
}
fn can_take_items(&self, _player: &dyn InventoryPlayer) -> BoxFuture<'_, bool> {
fn set_stack_prev(&self, stack: ItemStack, _previous_stack: ItemStack) -> BoxFuture<'_, ()> {
Box::pin(async move {
// TODO: Check enchantments
true
//TODO: this.entity.onEquipStack(this.equipmentSlot, previousStack, stack);
self.set_stack_no_callbacks(stack).await;
})
}
@@ -359,4 +447,17 @@ impl Slot for ArmorSlot {
self.inventory.mark_dirty();
})
}
/// Armor slots can only hold one item.
fn get_max_item_count(&self) -> BoxFuture<'_, u8> {
Box::pin(async move { 1 })
}
/// TODO: Check for curse of binding enchantment.
fn can_take_items(&self, _player: &dyn InventoryPlayer) -> BoxFuture<'_, bool> {
Box::pin(async move {
// TODO: Check enchantments
true
})
}
}

View File

@@ -1,3 +1,23 @@
//! Inventory synchronization handler.
//!
//! This module handles the synchronization of inventory state between the server
//! and connected clients. It ensures that players see the correct items in slots,
//! cursor items, and container properties (like furnace progress).
//!
//! # Synchronization
//!
//! The sync handler manages:
//! - Full container content updates (sent when opening a container or on major changes)
//! - Individual slot updates (sent when a single slot changes)
//! - Cursor item updates (the item being held by the mouse cursor)
//! - Property updates (container-specific data like furnace burn time)
//!
//! # Revision Tracking
//!
//! Each synchronization message includes a revision number to ensure the client
//! and server stay in sync. If the client detects a desync, it can request a full
//! resynchronization.
use std::sync::Arc;
use pumpkin_data::item_stack::ItemStack;
@@ -14,7 +34,18 @@ use tokio::sync::Mutex;
use crate::screen_handler::{InventoryPlayer, ScreenHandlerBehaviour};
/// Handles inventory synchronization to a specific player.
///
/// The sync handler stores a reference to the player and sends inventory
/// update packets when container state changes. It manages:
/// - Full content synchronization
/// - Incremental slot updates
/// - Cursor item tracking
/// - Property (UI element) updates
pub struct SyncHandler {
/// The player to synchronize inventory updates with.
///
/// None until `store_player` is called to attach a player.
player: Mutex<Option<Arc<dyn InventoryPlayer>>>,
}
@@ -25,6 +56,7 @@ impl Default for SyncHandler {
}
impl SyncHandler {
/// Creates a new sync handler with no player attached.
#[must_use]
pub fn new() -> Self {
Self {
@@ -32,10 +64,24 @@ impl SyncHandler {
}
}
/// Stores the player to synchronize with.
///
/// Must be called before any sync operations.
pub async fn store_player(&self, player: Arc<dyn InventoryPlayer>) {
self.player.lock().await.replace(player);
}
/// Sends a full container content update.
///
/// This sends all slots, the cursor item, and properties to the client.
/// Used for initial sync and recovery from desync.
///
/// # Arguments
/// - `screen_handler` - The screen handler to sync
/// - `stacks` - All slot contents
/// - `cursor_stack` - The item held by the cursor
/// - `properties` - Container property values
/// - `next_revision` - The new revision number
pub async fn update_state(
&self,
screen_handler: &ScreenHandlerBehaviour,
@@ -70,6 +116,15 @@ impl SyncHandler {
}
}
/// Updates a single slot on the client.
///
/// More efficient than full sync for single-slot changes.
///
/// # Arguments
/// - `screen_handler` - The screen handler
/// - `slot` - The slot index that changed
/// - `stack` - The new stack in that slot
/// - `next_revision` - The new revision number
pub async fn update_slot(
&self,
screen_handler: &ScreenHandlerBehaviour,
@@ -89,6 +144,13 @@ impl SyncHandler {
}
}
/// Updates the cursor item on the client.
///
/// Sent when the player's held (cursor) item changes.
///
/// # Arguments
/// - `screen_handler` - The screen handler
/// - `stack` - The new cursor item
pub async fn update_cursor_stack(
&self,
_screen_handler: &ScreenHandlerBehaviour,
@@ -103,6 +165,14 @@ impl SyncHandler {
}
}
/// Updates a container property on the client.
///
/// Used for UI elements like furnace progress bars.
///
/// # Arguments
/// - `screen_handler` - The screen handler
/// - `property` - The property index
/// - `value` - The new property value
pub async fn update_property(
&self,
screen_handler: &ScreenHandlerBehaviour,
@@ -121,29 +191,45 @@ impl SyncHandler {
}
}
// TrackedSlot in vanilla
/// Tracks the last known state of a slot for sync purposes.
///
/// Used to detect when a slot has changed and needs to be synced to the client.
/// Stores either the full stack or a hash for comparison.
#[derive(Clone)]
pub struct TrackedStack {
/// The full item stack last sent to the client.
///
/// Set when sending full stack data. Cleared when only sending a hash.
pub received_stack: Option<ItemStack>,
/// The hash of the item stack last sent to the client.
///
/// Used for lightweight comparison to detect changes.
pub received_hash: Option<OptionalItemStackHash>,
}
impl TrackedStack {
/// An empty tracked stack with no known state.
pub const EMPTY: Self = Self {
received_stack: None,
received_hash: None,
};
/// Records that we sent this stack to the client.
pub fn set_received_stack(&mut self, stack: ItemStack) {
self.received_stack = Some(stack);
self.received_hash = None;
}
/// Records that we sent this hash to the client.
pub fn set_received_hash(&mut self, hash: OptionalItemStackHash) {
self.received_hash = Some(hash);
self.received_stack = None;
}
/// Checks if the actual stack matches our tracked state.
///
/// Updates the tracked state to the actual stack if they match.
//FIX Methods named `is_*` normally take self by reference or no self. Consider choosing a less ambiguous name.
pub fn is_in_sync(&mut self, actual_stack: &ItemStack) -> bool {
if let Some(stack) = &self.received_stack {
return stack.are_equal(actual_stack);

View File

@@ -1,13 +1,43 @@
//! Window property definitions.
//!
//! This module defines container-specific UI properties that need to be synchronized
//! between server and client. These include progress bars, fuel indicators, and
//! other visual elements in container screens.
//!
//! # Window Properties
//!
//! Properties are identified by a unique ID and sent to the client to update
//! the container's visual state:
//! - Furnace: Fire icon animation, smelting progress
//! - Enchantment table: Level requirements, available enchantments
//! - Brewing stand: Brew time, fuel level
//! - Anvil: Repair cost
//!
//! See the Minecraft wiki for property ID mappings.
/// Trait for types that can be converted to window property IDs.
pub trait WindowPropertyTrait {
/// Converts this property to its protocol ID.
fn to_id(self) -> i16;
}
/// A window property with a specific value.
///
/// Used to send property updates to the client (e.g., furnace progress bar).
pub struct WindowProperty<T: WindowPropertyTrait> {
/// The property type being tracked (e.g., furnace fire icon, progress arrow).
window_property: T,
/// The current value of the property.
value: i16,
}
impl<T: WindowPropertyTrait> WindowProperty<T> {
/// Creates a new window property.
///
/// # Arguments
/// - `window_property` - The property type
/// - `value` - The property value
#[must_use]
pub const fn new(window_property: T, value: i16) -> Self {
Self {
window_property,
@@ -15,21 +45,34 @@ impl<T: WindowPropertyTrait> WindowProperty<T> {
}
}
/// Converts this property to a tuple of (id, value).
#[must_use]
pub fn into_tuple(self) -> (i16, i16) {
(self.window_property.to_id(), self.value)
}
}
/// Furnace window properties.
pub enum Furnace {
/// Fire icon animation level (0-250).
FireIcon,
/// Maximum fuel burn time.
MaximumFuelBurnTime,
/// Arrow progress animation (0-250).
ProgressArrow,
/// Maximum smelting progress time.
MaximumProgress,
}
/// Enchantment table window properties.
pub enum EnchantmentTable {
/// Experience level requirement for a specific slot.
LevelRequirement { slot: u8 },
/// Random seed for enchantment generation.
EnchantmentSeed,
/// Enchantment ID for a specific slot.
EnchantmentId { slot: u8 },
/// Enchantment level for a specific slot.
EnchantmentLevel { slot: u8 },
}
@@ -48,29 +91,45 @@ impl WindowPropertyTrait for EnchantmentTable {
})
}
}
/// Beacon window properties.
pub enum Beacon {
/// Effect power level (1-4).
PowerLevel,
/// First selected potion effect ID.
FirstPotionEffect,
/// Second selected potion effect ID.
SecondPotionEffect,
}
/// Anvil window properties.
pub enum Anvil {
/// Total repair cost in experience levels.
RepairCost,
}
/// Brewing stand window properties.
pub enum BrewingStand {
/// Brewing progress (0-400).
BrewTime,
/// Fuel time remaining (0-20).
FuelTime,
}
/// Stonecutter window properties.
pub enum Stonecutter {
/// ID of the selected recipe.
SelectedRecipe,
}
/// Loom window properties.
pub enum Loom {
/// ID of the selected pattern.
SelectedPattern,
}
/// Lectern window properties.
pub enum Lectern {
/// Current page number being viewed.
PageNumber,
}