1322 lines
51 KiB
Rust
1322 lines
51 KiB
Rust
//! 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,
|
|
slot::{NormalSlot, Slot},
|
|
sync_handler::{SyncHandler, TrackedStack},
|
|
};
|
|
use pumpkin_data::item_stack::ItemStack;
|
|
use pumpkin_data::{
|
|
data_component_impl::{EquipmentSlot, EquipmentType, EquippableImpl},
|
|
screen::WindowType,
|
|
};
|
|
use pumpkin_protocol::{
|
|
codec::item_stack_seralizer::OptionalItemStackHash,
|
|
java::{
|
|
client::play::{
|
|
CSetContainerContent, CSetContainerProperty, CSetContainerSlot, CSetCursorItem,
|
|
CSetPlayerInventory, CSetSelectedSlot,
|
|
},
|
|
server::play::SlotActionType,
|
|
},
|
|
};
|
|
use pumpkin_util::text::TextComponent;
|
|
use pumpkin_world::{
|
|
block::entities::PropertyDelegate,
|
|
inventory::{ComparableInventory, Inventory},
|
|
};
|
|
use std::sync::atomic::{AtomicU32, Ordering};
|
|
use std::{any::Any, collections::HashMap, sync::Arc};
|
|
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,
|
|
value: Arc<dyn PropertyDelegate>,
|
|
}
|
|
|
|
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)),
|
|
index,
|
|
value,
|
|
}
|
|
}
|
|
|
|
/// 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);
|
|
self.old_value = value;
|
|
has_changed
|
|
}
|
|
}
|
|
|
|
/// 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;
|
|
|
|
/// Checks if the player is in creative mode.
|
|
fn is_creative(&self) -> bool;
|
|
|
|
/// Gets the player's experience level.
|
|
fn experience_level(&self) -> i32;
|
|
|
|
/// Adds or removes experience levels.
|
|
fn add_experience_levels(&self, levels: i32) -> PlayerFuture<'_, ()>;
|
|
|
|
/// Gets the player's enchantment seed.
|
|
fn enchantment_seed(&self) -> i32;
|
|
|
|
/// Sets the player's enchantment seed.
|
|
fn set_enchantment_seed(&self, seed: i32) -> PlayerFuture<'_, ()>;
|
|
|
|
/// 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,
|
|
stack: &'a ItemStack,
|
|
) -> PlayerFuture<'a, ()>;
|
|
|
|
/// Awards experience points to the player (used for furnace smelting, etc.)
|
|
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
|
|
.get_inventory()
|
|
.offer_or_drop_stack(stack, player)
|
|
.await;
|
|
}
|
|
|
|
/// Type alias for async screen handler operations.
|
|
pub type ScreenHandlerFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
|
|
|
|
/// Future type that returns an `ItemStack` (used by `quick_move`).
|
|
pub type ItemStackFuture<'a> = ScreenHandlerFuture<'a, ItemStack>;
|
|
|
|
/// Future type that returns an optional slot index.
|
|
pub type OptionUsizeFuture<'a> = ScreenHandlerFuture<'a, Option<usize>>;
|
|
|
|
/// 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 ---
|
|
|
|
/// 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;
|
|
|
|
/// Returns this screen handler as a mutable Any reference.
|
|
fn as_any_mut(&mut self) -> &mut 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());
|
|
behaviour.slots.push(slot.clone());
|
|
behaviour.tracked_stacks.push(ItemStack::EMPTY.clone());
|
|
behaviour.previous_tracked_stacks.push(TrackedStack::EMPTY);
|
|
|
|
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 {
|
|
self.add_slot(Arc::new(NormalSlot::new(
|
|
player_inventory.clone(),
|
|
j + (i + 1) * 9,
|
|
)));
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 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() {
|
|
behaviour.previous_tracked_stacks[slot].set_received_hash(hash);
|
|
} else {
|
|
warn!(
|
|
"Incorrect slot index: {} available slots: {}",
|
|
slot,
|
|
behaviour.previous_tracked_stacks.len()
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 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 ---
|
|
|
|
/// 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,
|
|
) -> ScreenHandlerFuture<'a, ()> {
|
|
Box::pin(async move {
|
|
let behaviour = self.get_behaviour_mut();
|
|
|
|
// Lock and clone are performed inside the async block
|
|
let mut cursor_stack_lock = behaviour.cursor_stack.lock().await;
|
|
|
|
if !cursor_stack_lock.is_empty() {
|
|
offer_or_drop_stack(player, cursor_stack_lock.clone()).await;
|
|
*cursor_stack_lock = ItemStack::EMPTY.clone();
|
|
}
|
|
})
|
|
}
|
|
|
|
/// Drops all items from an inventory into the world.
|
|
fn drop_inventory<'a>(
|
|
&'a self,
|
|
player: &'a dyn InventoryPlayer,
|
|
inventory: Arc<dyn Inventory>,
|
|
) -> ScreenHandlerFuture<'a, ()> {
|
|
Box::pin(async move {
|
|
for i in 0..inventory.size() {
|
|
offer_or_drop_stack(player, inventory.remove_stack(i).await).await;
|
|
}
|
|
})
|
|
}
|
|
|
|
/// 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>>,
|
|
) -> ScreenHandlerFuture<'_, ()> {
|
|
Box::pin(async move {
|
|
let mut table: HashMap<ComparableInventory, HashMap<usize, usize>> = HashMap::new();
|
|
let other_binding = other.lock().await;
|
|
let other_behaviour = other_binding.get_behaviour();
|
|
|
|
for i in 0..other_behaviour.slots.len() {
|
|
let other_slot = other_behaviour.slots[i].clone();
|
|
let mut hash_map = HashMap::new();
|
|
hash_map.insert(other_slot.get_index(), i);
|
|
table.insert(
|
|
ComparableInventory(other_slot.get_inventory().clone()),
|
|
hash_map,
|
|
);
|
|
}
|
|
|
|
for i in 0..self.get_behaviour().slots.len() {
|
|
let slot = self.get_behaviour().slots[i].clone();
|
|
let inventory = slot.get_inventory();
|
|
let index = slot.get_index();
|
|
|
|
if let Some(hash_map) = table.get(&ComparableInventory(inventory.clone()))
|
|
&& let Some(other_index) = hash_map.get(&index)
|
|
{
|
|
self.get_behaviour_mut().tracked_stacks[i] =
|
|
other_behaviour.tracked_stacks[*other_index].clone();
|
|
self.get_behaviour_mut().previous_tracked_stacks[i] =
|
|
other_behaviour.previous_tracked_stacks[*other_index].clone();
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
/// 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();
|
|
let mut previous_tracked_stacks = Vec::new();
|
|
|
|
for i in 0..behaviour.slots.len() {
|
|
let stack = behaviour.slots[i].get_cloned_stack().await;
|
|
previous_tracked_stacks.push(stack.clone());
|
|
behaviour.previous_tracked_stacks[i].set_received_stack(stack);
|
|
}
|
|
|
|
let cursor_stack = behaviour.cursor_stack.lock().await.clone();
|
|
behaviour
|
|
.previous_cursor_stack
|
|
.set_received_stack(cursor_stack.clone());
|
|
|
|
for i in 0..behaviour.properties.len() {
|
|
let property_val = behaviour.properties[i].get();
|
|
behaviour.tracked_property_values[i] = property_val;
|
|
}
|
|
|
|
let next_revision = behaviour.next_revision();
|
|
|
|
if let Some(sync_handler) = behaviour.sync_handler.as_ref() {
|
|
sync_handler
|
|
.update_state(
|
|
behaviour,
|
|
&previous_tracked_stacks,
|
|
&cursor_stack,
|
|
behaviour.tracked_property_values.clone(),
|
|
next_revision,
|
|
)
|
|
.await;
|
|
}
|
|
})
|
|
}
|
|
|
|
/// Adds a listener for slot and property changes.
|
|
fn add_listener(
|
|
&mut self,
|
|
listener: Arc<dyn ScreenHandlerListener>,
|
|
) -> ScreenHandlerFuture<'_, ()> {
|
|
Box::pin(async move {
|
|
self.get_behaviour_mut().listeners.push(listener);
|
|
self.send_content_updates().await;
|
|
})
|
|
}
|
|
|
|
/// Attaches a sync handler and performs initial sync.
|
|
fn update_sync_handler(
|
|
&mut self,
|
|
sync_handler: Arc<SyncHandler>,
|
|
) -> ScreenHandlerFuture<'_, ()> {
|
|
Box::pin(async move {
|
|
let behaviour = self.get_behaviour_mut();
|
|
behaviour.sync_handler = Some(sync_handler.clone());
|
|
self.sync_state().await;
|
|
})
|
|
}
|
|
|
|
/// 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() {
|
|
let behaviour = self.get_behaviour_mut();
|
|
let slot = behaviour.slots[i].clone();
|
|
let stack = slot.get_cloned_stack().await;
|
|
self.update_tracked_slot(i, stack).await;
|
|
}
|
|
|
|
let behaviour = self.get_behaviour_mut();
|
|
let mut prop_vec = vec![];
|
|
for (idx, prop) in behaviour.properties.iter_mut().enumerate() {
|
|
let value = prop.get();
|
|
if prop.has_changed() {
|
|
prop_vec.push((idx, value));
|
|
}
|
|
}
|
|
|
|
for (idx, value) in prop_vec {
|
|
self.update_tracked_properties(idx as i32, value).await;
|
|
self.check_property_updates(idx as i32, value).await;
|
|
}
|
|
|
|
self.sync_state().await;
|
|
})
|
|
}
|
|
|
|
/// 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();
|
|
if idx <= behaviour.tracked_property_values.len() as i32 {
|
|
behaviour.tracked_property_values[idx as usize] = value;
|
|
for listener in &behaviour.listeners {
|
|
listener
|
|
.on_property_update(behaviour, idx as u8, value)
|
|
.await;
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
/// 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();
|
|
if !behaviour.disable_sync
|
|
&& let Some(old_value) = behaviour.tracked_property_values.get(idx as usize)
|
|
{
|
|
let old_value = *old_value;
|
|
if old_value != value {
|
|
behaviour
|
|
.tracked_property_values
|
|
.insert(idx as usize, value);
|
|
if let Some(ref sync_handler) = behaviour.sync_handler {
|
|
sync_handler.update_property(behaviour, idx, value).await;
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
/// Updates the tracked state of a slot.
|
|
fn update_tracked_slot(
|
|
&mut self,
|
|
slot: usize,
|
|
stack: ItemStack,
|
|
) -> ScreenHandlerFuture<'_, ()> {
|
|
Box::pin(async move {
|
|
let behaviour = self.get_behaviour_mut();
|
|
let other_stack = &behaviour.tracked_stacks[slot];
|
|
if !other_stack.are_equal(&stack) {
|
|
behaviour.tracked_stacks[slot] = stack.clone();
|
|
|
|
for listener in &behaviour.listeners {
|
|
listener
|
|
.on_slot_update(behaviour, slot as u8, stack.clone())
|
|
.await;
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
/// 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();
|
|
if !behaviour.disable_sync {
|
|
let prev_stack = &mut behaviour.previous_tracked_stacks[slot];
|
|
|
|
if !prev_stack.is_in_sync(&stack) {
|
|
prev_stack.set_received_stack(stack.clone());
|
|
let next_revision = behaviour.next_revision();
|
|
if let Some(sync_handler) = behaviour.sync_handler.as_ref() {
|
|
sync_handler
|
|
.update_slot(behaviour, slot, &stack, next_revision)
|
|
.await;
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
/// 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();
|
|
if !behaviour.disable_sync {
|
|
let cursor_stack = behaviour.cursor_stack.lock().await;
|
|
if !behaviour.previous_cursor_stack.is_in_sync(&cursor_stack) {
|
|
behaviour
|
|
.previous_cursor_stack
|
|
.set_received_stack(cursor_stack.clone());
|
|
if let Some(sync_handler) = behaviour.sync_handler.as_ref() {
|
|
sync_handler
|
|
.update_cursor_stack(behaviour, &cursor_stack)
|
|
.await;
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
/// 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();
|
|
|
|
for i in 0..slots_len {
|
|
let slot = self.get_behaviour().slots[i].clone();
|
|
let stack = slot.get_cloned_stack().await;
|
|
|
|
self.update_tracked_slot(i, stack.clone()).await;
|
|
self.check_slot_updates(i, stack).await;
|
|
}
|
|
|
|
self.check_cursor_stack_updates().await;
|
|
|
|
let behaviour = self.get_behaviour_mut();
|
|
let mut prop_vec = vec![];
|
|
for (idx, prop) in behaviour.properties.iter_mut().enumerate() {
|
|
let value = prop.get();
|
|
if prop.has_changed() {
|
|
prop_vec.push((idx, value));
|
|
}
|
|
}
|
|
|
|
for (idx, value) in prop_vec {
|
|
self.update_tracked_properties(idx as i32, value).await;
|
|
self.check_property_updates(idx as i32, value).await;
|
|
}
|
|
})
|
|
}
|
|
|
|
/// 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>,
|
|
slot: usize,
|
|
) -> OptionUsizeFuture<'a> {
|
|
Box::pin(async move {
|
|
(0..self.get_behaviour().slots.len()).find(|&i| {
|
|
Arc::ptr_eq(&self.get_behaviour().slots[i].get_inventory(), inventory)
|
|
&& self.get_behaviour().slots[i].get_index() == slot
|
|
})
|
|
})
|
|
}
|
|
|
|
/// 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>;
|
|
|
|
/// Handles a button click event (e.g., enchantment selection, beacon effects).
|
|
fn on_button_click<'a>(
|
|
&'a mut self,
|
|
_player: &'a dyn InventoryPlayer,
|
|
_button_id: i32,
|
|
) -> ScreenHandlerFuture<'a, bool> {
|
|
Box::pin(async { false })
|
|
}
|
|
|
|
/// 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,
|
|
start_index: i32,
|
|
end_index: i32,
|
|
from_last: bool,
|
|
) -> ScreenHandlerFuture<'a, bool> {
|
|
Box::pin(async move {
|
|
let mut success = false;
|
|
let mut current_index = if from_last {
|
|
end_index - 1
|
|
} else {
|
|
start_index
|
|
};
|
|
|
|
if stack.is_stackable() {
|
|
while !stack.is_empty()
|
|
&& (if from_last {
|
|
current_index >= start_index
|
|
} else {
|
|
current_index < end_index
|
|
})
|
|
{
|
|
let slot = self.get_behaviour().slots[current_index as usize].clone();
|
|
let slot_stack_lock = slot.get_stack().await;
|
|
let mut slot_stack = slot_stack_lock.lock().await;
|
|
|
|
if !slot_stack.is_empty() && slot_stack.are_items_and_components_equal(stack) {
|
|
let combined_count = slot_stack.item_count + stack.item_count;
|
|
let max_slot_count = slot.get_max_item_count_for_stack(&slot_stack).await;
|
|
if combined_count <= max_slot_count {
|
|
stack.set_count(0);
|
|
slot_stack.set_count(combined_count);
|
|
drop(slot_stack);
|
|
slot.mark_dirty().await;
|
|
success = true;
|
|
} else if slot_stack.item_count < max_slot_count {
|
|
stack.decrement(max_slot_count - slot_stack.item_count);
|
|
slot_stack.set_count(max_slot_count);
|
|
drop(slot_stack);
|
|
slot.mark_dirty().await;
|
|
success = true;
|
|
}
|
|
}
|
|
|
|
if from_last {
|
|
current_index -= 1;
|
|
} else {
|
|
current_index += 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
if !stack.is_empty() {
|
|
if from_last {
|
|
current_index = end_index - 1;
|
|
} else {
|
|
current_index = start_index;
|
|
}
|
|
|
|
while if from_last {
|
|
current_index >= start_index
|
|
} else {
|
|
current_index < end_index
|
|
} {
|
|
let slot = self.get_behaviour().slots[current_index as usize].clone();
|
|
let slot_stack = slot.get_stack().await;
|
|
let slot_stack = slot_stack.lock().await;
|
|
|
|
if slot_stack.is_empty() && slot.can_insert(stack).await {
|
|
let max_count = slot.get_max_item_count_for_stack(stack).await;
|
|
drop(slot_stack);
|
|
slot.set_stack(stack.split(max_count.min(stack.item_count)))
|
|
.await;
|
|
slot.mark_dirty().await;
|
|
success = true;
|
|
break;
|
|
}
|
|
|
|
if from_last {
|
|
current_index -= 1;
|
|
} else {
|
|
current_index += 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
success
|
|
})
|
|
}
|
|
|
|
/// 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,
|
|
_click_type: MouseClick,
|
|
_slot: Arc<dyn Slot>,
|
|
_slot_stack: ItemStack,
|
|
_cursor_stack: ItemStack,
|
|
) -> ScreenHandlerFuture<'a, bool> {
|
|
Box::pin(async {
|
|
// TODO: required for bundle in the future
|
|
false
|
|
})
|
|
}
|
|
|
|
/// Cancels any client-side changes and resynchronizes the state.
|
|
fn cancel(&mut self) -> ScreenHandlerFuture<'_, ()> {
|
|
Box::pin(async move {
|
|
self.sync_state().await;
|
|
})
|
|
}
|
|
|
|
/// Public entry point for slot click handling.
|
|
fn on_slot_click<'a>(
|
|
&'a mut self,
|
|
slot_index: i32,
|
|
button: i32,
|
|
action_type: SlotActionType,
|
|
player: &'a dyn InventoryPlayer,
|
|
) -> ScreenHandlerFuture<'a, ()> {
|
|
Box::pin(async move {
|
|
self.internal_on_slot_click(slot_index, button, action_type, player)
|
|
.await;
|
|
})
|
|
}
|
|
|
|
/// 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,
|
|
slot_index: i32,
|
|
button: i32,
|
|
action_type: SlotActionType,
|
|
player: &'a dyn InventoryPlayer,
|
|
) -> ScreenHandlerFuture<'a, ()> {
|
|
Box::pin(async move {
|
|
if action_type == SlotActionType::PickupAll && button == 0 {
|
|
let behavior = self.get_behaviour_mut();
|
|
let mut cursor_stack = behavior.cursor_stack.lock().await;
|
|
let mut to_pick_up = cursor_stack.get_max_stack_size() - cursor_stack.item_count;
|
|
|
|
for slot in &behavior.slots {
|
|
if to_pick_up == 0 {
|
|
break;
|
|
}
|
|
|
|
let item_stack = slot.get_cloned_stack().await;
|
|
if !item_stack.are_items_and_components_equal(&cursor_stack) {
|
|
continue;
|
|
}
|
|
|
|
if !slot.allow_modification(player).await {
|
|
continue;
|
|
}
|
|
|
|
let taken_stack = slot
|
|
.safe_take(
|
|
item_stack.item_count.min(to_pick_up),
|
|
cursor_stack.get_max_stack_size() - cursor_stack.item_count,
|
|
player,
|
|
)
|
|
.await;
|
|
to_pick_up -= taken_stack.item_count;
|
|
cursor_stack.increment(taken_stack.item_count);
|
|
}
|
|
} else if action_type == SlotActionType::QuickCraft {
|
|
let drag_type = button & 3;
|
|
let drag_button = (button >> 2) & 3;
|
|
let behaviour = self.get_behaviour_mut();
|
|
if drag_type == 0 {
|
|
behaviour.drag_slots.clear();
|
|
} else if drag_type == 1 {
|
|
if slot_index < 0 {
|
|
warn!("Invalid slot index for drag action: {slot_index}. Must be >= 0");
|
|
return;
|
|
}
|
|
let cursor_stack = behaviour.cursor_stack.lock().await;
|
|
|
|
let slot = &behaviour.slots[slot_index as usize];
|
|
let stack_lock = slot.get_stack().await;
|
|
let stack = stack_lock.lock().await;
|
|
if !cursor_stack.is_empty()
|
|
&& slot.can_insert(&cursor_stack).await
|
|
&& (stack.are_items_and_components_equal(&cursor_stack) || stack.is_empty())
|
|
&& slot.get_max_item_count_for_stack(&stack).await > stack.item_count
|
|
{
|
|
behaviour.drag_slots.push(slot_index as u32);
|
|
}
|
|
} else if drag_type == 2 && !behaviour.drag_slots.is_empty() {
|
|
// process drag end
|
|
if behaviour.drag_slots.len() == 1 {
|
|
let slot = behaviour.drag_slots[0] as i32;
|
|
behaviour.drag_slots.clear();
|
|
let _ = behaviour;
|
|
self.internal_on_slot_click(
|
|
slot,
|
|
drag_button,
|
|
SlotActionType::Pickup,
|
|
player,
|
|
)
|
|
.await;
|
|
|
|
return;
|
|
}
|
|
if drag_button == 2 && !player.has_infinite_materials() {
|
|
return; // Only creative
|
|
}
|
|
|
|
let mut cursor_stack = behaviour.cursor_stack.lock().await;
|
|
let initial_count = cursor_stack.item_count;
|
|
for slot_index in &behaviour.drag_slots {
|
|
let slot = behaviour.slots[*slot_index as usize].clone();
|
|
let stack_lock = slot.get_stack().await;
|
|
let stack = stack_lock.lock().await;
|
|
|
|
if (stack.are_items_and_components_equal(&cursor_stack) || stack.is_empty())
|
|
&& slot.can_insert(&cursor_stack).await
|
|
{
|
|
let mut inserting_count = if drag_button == 0 {
|
|
initial_count / behaviour.drag_slots.len() as u8
|
|
} else if drag_button == 1 {
|
|
1
|
|
} else if drag_button == 2 {
|
|
cursor_stack.item_count = cursor_stack.get_max_stack_size();
|
|
cursor_stack.item_count
|
|
} else {
|
|
panic!("Invalid drag button: {drag_button}");
|
|
};
|
|
inserting_count = inserting_count
|
|
.min(max(
|
|
0,
|
|
slot.get_max_item_count_for_stack(&stack).await
|
|
- stack.item_count,
|
|
))
|
|
.min(cursor_stack.item_count);
|
|
if inserting_count > 0 {
|
|
let mut stack_clone = stack.clone();
|
|
drop(stack);
|
|
if stack_clone.is_empty() {
|
|
stack_clone = cursor_stack.copy_with_count(0);
|
|
}
|
|
stack_clone.increment(inserting_count);
|
|
slot.set_stack(stack_clone).await;
|
|
if drag_button != 2 {
|
|
cursor_stack.decrement(inserting_count);
|
|
}
|
|
if cursor_stack.is_empty() {
|
|
*cursor_stack = ItemStack::EMPTY.clone();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if drag_button == 2 {
|
|
*cursor_stack = ItemStack::EMPTY.clone();
|
|
}
|
|
behaviour.drag_slots.clear();
|
|
}
|
|
} else if action_type == SlotActionType::Throw {
|
|
if slot_index >= 0 && self.get_behaviour().cursor_stack.lock().await.is_empty() {
|
|
let slot = self.get_behaviour().slots[slot_index as usize].clone();
|
|
let prev_stack = slot.get_cloned_stack().await;
|
|
if !prev_stack.is_empty() {
|
|
if button == 1 {
|
|
// Throw all
|
|
while slot
|
|
.get_cloned_stack()
|
|
.await
|
|
.are_items_and_components_equal(&prev_stack)
|
|
{
|
|
let drop_stack =
|
|
slot.safe_take(prev_stack.item_count, u8::MAX, player).await;
|
|
player.drop_item(drop_stack, true).await;
|
|
// player.handleCreativeModeItemDrop(itemStack);
|
|
}
|
|
} else {
|
|
let drop_stack = slot.safe_take(1, u8::MAX, player).await;
|
|
if !drop_stack.is_empty() {
|
|
slot.on_take_item(player, &drop_stack).await;
|
|
player.drop_item(drop_stack, true).await;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else if action_type == SlotActionType::Clone {
|
|
if player.has_infinite_materials() && slot_index >= 0 {
|
|
let behaviour = self.get_behaviour_mut();
|
|
let mut cursor_stack = behaviour.cursor_stack.lock().await;
|
|
if !cursor_stack.is_empty() {
|
|
return;
|
|
}
|
|
let slot = behaviour.slots[slot_index as usize].clone();
|
|
let stack_lock = slot.get_stack().await;
|
|
let stack = stack_lock.lock().await;
|
|
*cursor_stack = stack.copy_with_count(stack.get_max_stack_size());
|
|
}
|
|
} else if (action_type == SlotActionType::Pickup
|
|
|| action_type == SlotActionType::QuickMove)
|
|
&& (button == 0 || button == 1)
|
|
{
|
|
let click_type = if button == 0 {
|
|
MouseClick::Left
|
|
} else {
|
|
MouseClick::Right
|
|
};
|
|
|
|
// Drop item if outside inventory
|
|
if slot_index == SLOT_INDEX_OUTSIDE {
|
|
let mut cursor_stack = self.get_behaviour().cursor_stack.lock().await;
|
|
if !cursor_stack.is_empty() {
|
|
if click_type == MouseClick::Left {
|
|
player.drop_item(cursor_stack.clone(), true).await;
|
|
*cursor_stack = ItemStack::EMPTY.clone();
|
|
} else {
|
|
player.drop_item(cursor_stack.split(1), true).await;
|
|
}
|
|
}
|
|
} else if action_type == SlotActionType::QuickMove {
|
|
if slot_index < 0 {
|
|
return;
|
|
}
|
|
|
|
let slot = self.get_behaviour().slots[slot_index as usize].clone();
|
|
|
|
if !slot.can_take_items(player).await {
|
|
return;
|
|
}
|
|
|
|
let mut moved_stack = self.quick_move(player, slot_index).await;
|
|
|
|
while !moved_stack.is_empty()
|
|
&& ItemStack::are_items_and_components_equal(
|
|
&slot.get_cloned_stack().await,
|
|
&moved_stack,
|
|
)
|
|
{
|
|
moved_stack = self.quick_move(player, slot_index).await;
|
|
}
|
|
} else {
|
|
// Pickup
|
|
if slot_index < 0 {
|
|
return;
|
|
}
|
|
|
|
let slot = self.get_behaviour().slots[slot_index as usize].clone();
|
|
|
|
if click_type == MouseClick::Left {
|
|
slot.on_click(player).await;
|
|
}
|
|
|
|
let slot_stack = slot.get_cloned_stack().await;
|
|
let mut cursor_stack = self.get_behaviour().cursor_stack.lock().await;
|
|
|
|
let equipment_slot = cursor_stack
|
|
.get_data_component::<EquippableImpl>()
|
|
.map_or(&EquipmentSlot::MAIN_HAND, |equippable| equippable.slot);
|
|
|
|
if self
|
|
.handle_slot_click(
|
|
player,
|
|
click_type.clone(),
|
|
slot.clone(),
|
|
slot_stack.clone(),
|
|
cursor_stack.clone(),
|
|
)
|
|
.await
|
|
{
|
|
return;
|
|
}
|
|
|
|
if slot_stack.is_empty() {
|
|
if !cursor_stack.is_empty() {
|
|
if equipment_slot.slot_type() == EquipmentType::HumanoidArmor
|
|
&& (5..9).contains(&slot_index)
|
|
{
|
|
player
|
|
.enqueue_equipment_change(equipment_slot, &cursor_stack)
|
|
.await;
|
|
}
|
|
|
|
let transfer_count = if click_type == MouseClick::Left {
|
|
cursor_stack.item_count
|
|
} else {
|
|
1
|
|
};
|
|
*cursor_stack = slot
|
|
.insert_stack_count(cursor_stack.clone(), transfer_count)
|
|
.await;
|
|
}
|
|
} else if slot.can_take_items(player).await {
|
|
if cursor_stack.is_empty() {
|
|
let take_count = if click_type == MouseClick::Left {
|
|
slot_stack.item_count
|
|
} else {
|
|
slot_stack.item_count.div_ceil(2)
|
|
};
|
|
let taken =
|
|
slot.try_take_stack_range(take_count, u8::MAX, player).await;
|
|
if let Some(taken) = taken {
|
|
// Reverse order of operations, shouldn't affect anything
|
|
*cursor_stack = taken.clone();
|
|
slot.on_take_item(player, &taken).await;
|
|
|
|
if (5..9).contains(&slot_index) {
|
|
let equipment_slot = cursor_stack
|
|
.get_data_component::<EquippableImpl>()
|
|
.map_or(&EquipmentSlot::MAIN_HAND, |equippable| {
|
|
equippable.slot
|
|
});
|
|
player
|
|
.enqueue_equipment_change(equipment_slot, ItemStack::EMPTY)
|
|
.await;
|
|
}
|
|
}
|
|
} else if slot.can_insert(&cursor_stack).await {
|
|
if equipment_slot.slot_type() == EquipmentType::HumanoidArmor
|
|
&& (5..9).contains(&slot_index)
|
|
{
|
|
player
|
|
.enqueue_equipment_change(equipment_slot, &cursor_stack)
|
|
.await;
|
|
}
|
|
|
|
if ItemStack::are_items_and_components_equal(&slot_stack, &cursor_stack)
|
|
{
|
|
let insert_count = if click_type == MouseClick::Left {
|
|
cursor_stack.item_count
|
|
} else {
|
|
1
|
|
};
|
|
*cursor_stack = slot
|
|
.insert_stack_count(cursor_stack.clone(), insert_count)
|
|
.await;
|
|
} else if cursor_stack.item_count
|
|
<= slot.get_max_item_count_for_stack(&cursor_stack).await
|
|
{
|
|
let old_cursor_stack = cursor_stack.clone();
|
|
*cursor_stack = slot_stack.clone();
|
|
slot.set_stack(old_cursor_stack).await;
|
|
}
|
|
} else if ItemStack::are_items_and_components_equal(
|
|
&slot_stack,
|
|
&cursor_stack,
|
|
) {
|
|
let taken = slot
|
|
.try_take_stack_range(
|
|
slot_stack.item_count,
|
|
cursor_stack
|
|
.get_max_stack_size()
|
|
.saturating_sub(cursor_stack.item_count),
|
|
player,
|
|
)
|
|
.await;
|
|
|
|
if let Some(taken) = taken {
|
|
cursor_stack.increment(taken.item_count);
|
|
slot.on_take_item(player, &taken).await;
|
|
}
|
|
}
|
|
}
|
|
|
|
slot.mark_dirty().await;
|
|
}
|
|
} else if action_type == SlotActionType::Swap && (0..9).contains(&button)
|
|
|| button == 40
|
|
{
|
|
let mut button_stack = player
|
|
.get_inventory()
|
|
.get_stack(button as usize)
|
|
.await
|
|
.lock()
|
|
.await
|
|
.clone();
|
|
let source_slot = self.get_behaviour().slots[slot_index as usize].clone();
|
|
let source_stack = source_slot.get_cloned_stack().await;
|
|
|
|
if !button_stack.is_empty() || !source_stack.is_empty() {
|
|
if button_stack.is_empty() {
|
|
if source_slot.can_take_items(player).await {
|
|
player
|
|
.get_inventory()
|
|
.set_stack(button as usize, source_stack.clone())
|
|
.await;
|
|
source_slot.set_stack(ItemStack::EMPTY.clone()).await;
|
|
source_slot.on_take_item(player, &source_stack).await;
|
|
}
|
|
} else if source_stack.is_empty() && source_slot.can_insert(&button_stack).await
|
|
{
|
|
let max_count = source_slot
|
|
.get_max_item_count_for_stack(&button_stack)
|
|
.await;
|
|
if button_stack.item_count > max_count {
|
|
// button_stack might need to be a ref instead of a clone
|
|
source_slot.set_stack(button_stack.split(max_count)).await;
|
|
} else {
|
|
player
|
|
.get_inventory()
|
|
.set_stack(button as usize, ItemStack::EMPTY.clone())
|
|
.await;
|
|
source_slot.set_stack(button_stack).await;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
pub trait ScreenHandlerListener: Send + Sync {
|
|
fn on_slot_update<'a>(
|
|
&'a self,
|
|
_screen_handler: &'a ScreenHandlerBehaviour,
|
|
_slot: u8,
|
|
_stack: ItemStack,
|
|
) -> Pin<Box<dyn Future<Output = ()> + Send + 'a>> {
|
|
Box::pin(async {})
|
|
}
|
|
fn on_property_update<'a>(
|
|
&'a self,
|
|
_screen_handler: &'a ScreenHandlerBehaviour,
|
|
_property: u8,
|
|
_value: i32,
|
|
) -> Pin<Box<dyn Future<Output = ()> + Send + 'a>> {
|
|
Box::pin(async {})
|
|
}
|
|
}
|
|
|
|
pub type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
|
|
|
|
pub type SharedScreenHandler = Arc<Mutex<dyn ScreenHandler>>;
|
|
|
|
pub trait ScreenHandlerFactory: Send + Sync {
|
|
fn create_screen_handler<'a>(
|
|
&'a self,
|
|
sync_id: u8,
|
|
player_inventory: &'a Arc<PlayerInventory>,
|
|
player: &'a dyn InventoryPlayer,
|
|
) -> BoxFuture<'a, Option<SharedScreenHandler>>;
|
|
fn get_display_name(&self) -> TextComponent;
|
|
}
|
|
|
|
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>,
|
|
/// Whether players can grab items out of the inventory.
|
|
pub allow_grab_items: bool,
|
|
/// Whether players can put items into the inventory from their own.
|
|
pub allow_put_items: bool,
|
|
/// Number of slots that belong to the container (not the player inventory).
|
|
pub container_slots: usize,
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
pub enum ClickType {
|
|
Left,
|
|
Right,
|
|
ShiftLeft,
|
|
ShiftRight,
|
|
Middle,
|
|
Drop,
|
|
ControlDrop,
|
|
DoubleClick,
|
|
NumberKey(u8),
|
|
Unknown,
|
|
}
|
|
|
|
impl ScreenHandlerBehaviour {
|
|
#[must_use]
|
|
pub fn new(sync_id: u8, window_type: Option<WindowType>) -> Self {
|
|
Self {
|
|
slots: Vec::new(),
|
|
sync_id,
|
|
listeners: Vec::new(),
|
|
sync_handler: None,
|
|
tracked_stacks: Vec::new(),
|
|
cursor_stack: Arc::new(Mutex::new(ItemStack::EMPTY.clone())),
|
|
previous_tracked_stacks: Vec::new(),
|
|
previous_cursor_stack: TrackedStack::EMPTY,
|
|
revision: AtomicU32::new(0),
|
|
disable_sync: false,
|
|
properties: Vec::new(),
|
|
tracked_property_values: Vec::new(),
|
|
window_type,
|
|
drag_slots: Vec::new(),
|
|
allow_grab_items: true,
|
|
allow_put_items: true,
|
|
container_slots: 0,
|
|
}
|
|
}
|
|
|
|
pub fn next_revision(&self) -> u32 {
|
|
self.revision.fetch_add(1, Ordering::Relaxed);
|
|
self.revision.fetch_and(32767, Ordering::Relaxed) & 32767
|
|
}
|
|
}
|