feat(plugin-api): add some more events

This commit is contained in:
Alexander Medvedev 2026-04-26 13:49:13 +02:00
parent ee11ba746f
commit 7f21c3cfe2
33 changed files with 1132 additions and 268 deletions

1
Cargo.lock generated
View File

@ -2795,6 +2795,7 @@ dependencies = [
"bytes",
"console-subscriber",
"crossbeam",
"dashmap",
"flate2",
"futures",
"hmac 0.13.0",

View File

@ -0,0 +1,189 @@
use crate::{
player::player_inventory::PlayerInventory,
screen_handler::{
InventoryPlayer, ItemStackFuture, ScreenHandler, ScreenHandlerBehaviour,
ScreenHandlerFuture,
},
slot::NormalSlot,
};
use pumpkin_data::{item_stack::ItemStack, screen::WindowType};
use pumpkin_world::inventory::Inventory;
use std::{any::Any, sync::Arc};
/// Builder for custom GUIs.
pub struct GUIBuilder {
screen_type: WindowType,
rows: u8,
columns: u8,
inventory: Arc<dyn Inventory>,
allow_grab_items: bool,
allow_put_items: bool,
}
impl GUIBuilder {
/// Creates a new `GUIBuilder` with a basic 9x3 layout.
pub fn new(screen_type: WindowType, inventory: Arc<dyn Inventory>) -> Self {
let (rows, columns) = match screen_type {
WindowType::Generic9x1 => (1, 9),
WindowType::Generic9x2 => (2, 9),
WindowType::Generic9x4 => (4, 9),
WindowType::Generic9x5 => (5, 9),
WindowType::Generic9x6 => (6, 9),
WindowType::Generic3x3 => (3, 3),
WindowType::Hopper => (1, 5),
_ => (3, 9), // Default to 9x3
};
Self {
screen_type,
rows,
columns,
inventory,
allow_grab_items: true,
allow_put_items: true,
}
}
/// Sets whether players can grab items out of the inventory.
#[must_use]
pub const fn allow_grab_items(mut self, allow: bool) -> Self {
self.allow_grab_items = allow;
self
}
/// Sets whether players can put items into the inventory from their own.
#[must_use]
pub const fn allow_put_items(mut self, allow: bool) -> Self {
self.allow_put_items = allow;
self
}
/// Builds the `GUIScreenHandler`.
pub async fn build(
self,
sync_id: u8,
player_inventory: &Arc<PlayerInventory>,
) -> GUIScreenHandler {
let mut behaviour = ScreenHandlerBehaviour::new(sync_id, Some(self.screen_type));
behaviour.allow_grab_items = self.allow_grab_items;
behaviour.allow_put_items = self.allow_put_items;
behaviour.container_slots = (self.rows * self.columns) as usize;
let mut handler = GUIScreenHandler {
inventory: self.inventory.clone(),
rows: self.rows,
columns: self.columns,
behaviour,
};
self.inventory.on_open().await;
handler.add_inventory_slots();
let player_inventory_trait: Arc<dyn Inventory> = player_inventory.clone();
handler.add_player_slots(&player_inventory_trait);
handler
}
}
pub struct GUIScreenHandler {
pub inventory: Arc<dyn Inventory>,
pub rows: u8,
pub columns: u8,
behaviour: ScreenHandlerBehaviour,
}
impl GUIScreenHandler {
fn add_inventory_slots(&mut self) {
for i in 0..self.rows {
for j in 0..self.columns {
self.add_slot(Arc::new(NormalSlot::new(
self.inventory.clone(),
(j + i * self.columns) as usize,
)));
}
}
}
}
impl ScreenHandler for GUIScreenHandler {
fn as_any(&self) -> &dyn Any {
self
}
fn get_behaviour(&self) -> &ScreenHandlerBehaviour {
&self.behaviour
}
fn get_behaviour_mut(&mut self) -> &mut ScreenHandlerBehaviour {
&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;
})
}
fn quick_move<'a>(
&'a mut self,
_player: &'a dyn InventoryPlayer,
slot_index: i32,
) -> ItemStackFuture<'a> {
Box::pin(async move {
let mut stack_left = ItemStack::EMPTY.clone();
let slot = self.get_behaviour().slots[slot_index as usize].clone();
if slot.has_stack().await {
let slot_stack_lock = slot.get_stack().await;
let slot_stack_guard = slot_stack_lock.lock().await;
stack_left = slot_stack_guard.clone();
drop(slot_stack_guard);
let mut slot_stack_mut = slot_stack_lock.lock().await;
let container_slots = i32::from(self.rows * self.columns);
if slot_index < container_slots {
// From container to player
if !self.get_behaviour().allow_grab_items {
return ItemStack::EMPTY.clone();
}
if !self
.insert_item(
&mut slot_stack_mut,
container_slots,
self.get_behaviour().slots.len() as i32,
true,
)
.await
{
return ItemStack::EMPTY.clone();
}
} else {
// From player to container
if !self.get_behaviour().allow_put_items {
return ItemStack::EMPTY.clone();
}
if !self
.insert_item(&mut slot_stack_mut, 0, container_slots, false)
.await
{
// Move from player area to inventory (start)
return ItemStack::EMPTY.clone();
}
}
if slot_stack_mut.is_empty() {
drop(slot_stack_mut);
slot.set_stack(ItemStack::EMPTY.clone()).await;
} else {
drop(slot_stack_mut);
slot.mark_dirty().await;
}
}
stack_left
})
}
}

View File

@ -39,6 +39,7 @@ pub mod entity_equipment;
mod error;
pub mod furnace_like;
pub mod generic_container_screen_handler;
pub mod gui_builder;
pub mod player;
pub mod screen_handler;
pub mod slot;

View File

@ -772,6 +772,13 @@ pub trait ScreenHandler: Send + Sync {
})
}
/// Cancels any client-side changes and resynchronizes the state.
fn cancel<'a>(&'a mut self, _player: &'a dyn InventoryPlayer) -> ScreenHandlerFuture<'a, ()> {
Box::pin(async move {
self.sync_state().await;
})
}
/// Public entry point for slot click handling.
fn on_slot_click<'a>(
&'a mut self,
@ -1234,6 +1241,26 @@ pub struct ScreenHandlerBehaviour {
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 {
@ -1254,6 +1281,9 @@ impl ScreenHandlerBehaviour {
tracked_property_values: Vec::new(),
window_type,
drag_slots: Vec::new(),
allow_grab_items: true,
allow_put_items: true,
container_slots: 0,
}
}

View File

@ -67,6 +67,20 @@ interface common {
count: u8,
}
/// Represents the type of click interaction in a GUI.
enum click-type {
left,
right,
shift-left,
shift-right,
middle,
drop,
control-drop,
double-click,
number-key,
unknown,
}
/// Represents the various poses an entity can be in.
enum entity-pose {
standing,

View File

@ -1,5 +1,5 @@
interface event {
use common.{block-position, game-mode, hand, item-stack, position};
use common.{block-position, game-mode, hand, item-stack, position, click-type};
use player.{player};
use text.{text-component};
use %world.{%world};
@ -179,6 +179,23 @@ interface event {
cancelled: bool
}
record inventory-click-event-data {
player: player,
window-type: option<string>,
click-type: click-type,
slot: s16,
raw-slot: s16,
clicked-item: option<item-stack>,
cursor: option<item-stack>,
hotbar-button: s32,
cancelled: bool
}
record inventory-close-event-data {
player: player,
window-type: option<string>
}
record block-redstone-event-data {
target-world: %world,
state-id: u16,
@ -272,6 +289,8 @@ interface event {
player-toggle-sneak-event,
player-toggle-flight-event,
player-toggle-sprint-event,
inventory-click-event,
inventory-close-event,
block-redstone-event,
block-break-event,
block-burn-event,
@ -305,6 +324,8 @@ interface event {
player-toggle-sneak-event(player-toggle-sneak-event-data),
player-toggle-flight-event(player-toggle-flight-event-data),
player-toggle-sprint-event(player-toggle-sprint-event-data),
inventory-click-event(inventory-click-event-data),
inventory-close-event(inventory-close-event-data),
block-redstone-event(block-redstone-event-data),
block-break-event(block-break-event-data),
block-burn-event(block-burn-event-data),

View File

@ -1,6 +1,6 @@
interface gui {
use text.{text-component};
use common.{item-stack};
use common.{item-stack, click-type};
enum gui-type {
generic-9x1,
@ -46,5 +46,17 @@ interface gui {
/// Clears all items from the GUI.
clear-items: func();
/// Sets whether players can grab items out of the inventory.
set-allow-grab-items: func(allow: bool);
/// Returns whether players can grab items out of the inventory.
get-allow-grab-items: func() -> bool;
/// Sets whether players can put items into the inventory from their own.
set-allow-put-items: func(allow: bool);
/// Returns whether players can put items into the inventory from their own.
get-allow-put-items: func() -> bool;
}
}

View File

@ -29,6 +29,7 @@ pumpkin-data.workspace = true
pumpkin-protocol.workspace = true
pumpkin-macros.workspace = true
dashmap.workspace = true
crossbeam.workspace = true
uuid.workspace = true
tokio = { workspace = true, features = [

View File

@ -68,7 +68,7 @@ impl LookControl {
self.look_at_timer = 2;
}
pub async fn tick(&mut self, mob: &dyn Mob) {
pub fn tick(&mut self, mob: &dyn Mob) {
let entity = mob.get_entity();
if Self::should_stay_horizontal() {
entity.set_pitch(0.0);
@ -98,17 +98,18 @@ impl LookControl {
));
}
self.clamp_head_yaw(mob).await;
Self::clamp_head_yaw(mob);
}
const fn should_stay_horizontal() -> bool {
true
}
async fn clamp_head_yaw(&self, mob: &dyn Mob) {
fn clamp_head_yaw(mob: &dyn Mob) {
let mob_entity = mob.get_mob_entity();
let navigator = mob_entity.navigator.lock().await;
if !navigator.is_idle() {
if let Ok(navigator) = mob_entity.navigator.try_lock()
&& !navigator.is_idle()
{
let entity = &mob_entity.living_entity.entity;
let max_head_rotation = mob.get_max_head_rotation();
entity.head_yaw.store(clamp_angle(

View File

@ -75,9 +75,9 @@ impl Goal for MeleeAttackGoal {
fn should_continue<'a>(&'a self, mob: &'a dyn Mob) -> GoalFuture<'a, bool> {
Box::pin(async {
let target = mob.get_mob_entity().target.lock().await;
let target = mob.get_mob_entity().target.lock().await.clone();
let Some(target) = target.as_ref() else {
let Some(target) = target else {
return false;
};
if !target.get_entity().is_alive() {
@ -85,7 +85,12 @@ impl Goal for MeleeAttackGoal {
}
if !self.pause_when_mob_idle {
return !mob.get_mob_entity().navigator.lock().await.is_idle();
let is_idle = mob
.get_mob_entity()
.navigator
.try_lock()
.is_ok_and(|navigator| navigator.is_idle());
return !is_idle;
}
let is_valid_target = !target
@ -104,7 +109,8 @@ impl Goal for MeleeAttackGoal {
Box::pin(async {
// TODO: add missing fields like mob attacking to true and correct Navigation methods
if let Some(target) = mob.get_mob_entity().target.lock().await.as_ref() {
let target = mob.get_mob_entity().target.lock().await.clone();
if let Some(target) = target {
let mut navigator = mob.get_mob_entity().navigator.lock().await;
let target_pos = target.get_entity().pos.load();
navigator.set_progress(NavigatorGoal {
@ -144,8 +150,8 @@ impl Goal for MeleeAttackGoal {
fn tick<'a>(&'a mut self, mob: &'a dyn Mob) -> GoalFuture<'a, ()> {
Box::pin(async {
let target_lock = mob.get_mob_entity().target.lock().await;
let Some(target) = target_lock.as_ref() else {
let target = mob.get_mob_entity().target.lock().await.clone();
let Some(target) = target else {
return;
};
@ -153,7 +159,7 @@ impl Goal for MeleeAttackGoal {
.look_control
.lock()
.await
.look_at_entity_with_range(target, 30.0, 30.0);
.look_at_entity_with_range(&target, 30.0, 30.0);
self.update_countdown_ticks = (self.update_countdown_ticks - 1).max(0);

View File

@ -13,7 +13,7 @@ use crate::entity::ai::pathfinder::walk_node_evaluator::WalkNodeEvaluator;
use pumpkin_data::attributes::Attributes;
use pumpkin_util::math::wrap_degrees;
use std::collections::HashMap;
use std::sync::atomic::Ordering;
use std::sync::atomic::{AtomicBool, Ordering};
pub mod binary_heap;
pub mod node;
@ -61,6 +61,9 @@ pub struct Navigator {
// Reusable allocations to avoid per-pathfind heap allocations
open_set: BinaryHeap,
neighbors_buf: Vec<Node>,
/// Thread-safe status check to avoid deadlocks when components (like `LookControl`) need to
/// check navigation status.
pub is_idle: AtomicBool,
}
impl Default for Navigator {
@ -79,6 +82,7 @@ impl Default for Navigator {
repath_cooldown: 0,
open_set: BinaryHeap::new(),
neighbors_buf: Vec::new(),
is_idle: AtomicBool::new(true),
}
}
}
@ -95,6 +99,7 @@ const MAX_YAW_TURN_PER_TICK: f32 = 90.0;
impl Navigator {
pub fn set_progress(&mut self, goal: NavigatorGoal) {
self.is_idle.store(false, Ordering::Relaxed);
self.current_goal = Some(goal);
self.current_path = None;
}
@ -106,6 +111,7 @@ impl Navigator {
}
pub fn stop(&mut self) {
self.is_idle.store(true, Ordering::Relaxed);
self.current_goal = None;
self.current_path = None;
self.ticks_on_current_node = 0;
@ -300,11 +306,13 @@ impl Navigator {
pub async fn tick(&mut self, entity: &LivingEntity) {
let Some(goal) = self.current_goal.take() else {
// Idle: stop the mob
self.is_idle.store(true, Ordering::Relaxed);
entity.movement_input.store(Vector3::new(0.0, 0.0, 0.0));
return;
};
if goal.current_progress == goal.destination {
self.is_idle.store(true, Ordering::Relaxed);
self.current_path = None;
entity.movement_input.store(Vector3::new(0.0, 0.0, 0.0));
return;
@ -437,6 +445,7 @@ impl Navigator {
.store(false, std::sync::atomic::Ordering::SeqCst);
}
} else {
self.is_idle.store(true, Ordering::Relaxed);
self.current_path = None;
entity.movement_input.store(Vector3::new(0.0, 0.0, 0.0));
}
@ -446,7 +455,7 @@ impl Navigator {
}
#[must_use]
pub const fn is_idle(&self) -> bool {
self.current_goal.is_none()
pub fn is_idle(&self) -> bool {
self.is_idle.load(Ordering::Relaxed)
}
}

View File

@ -1304,10 +1304,16 @@ impl LivingEntity {
let block_pos = self.entity.block_pos.load();
for slot in self.equipment_slots.values() {
let armor_slots: Vec<Arc<Mutex<ItemStack>>> = {
let equipment_lock = self.entity_equipment.lock().await;
self.equipment_slots
.values()
.map(|slot| equipment_lock.get(slot))
.collect()
};
for equipment in armor_slots {
let item = {
let lock = self.entity_equipment.lock().await;
let equipment = lock.get(slot);
let mut item_lock = equipment.lock().await;
mem::replace(&mut *item_lock, ItemStack::EMPTY.clone())
};
@ -1532,12 +1538,16 @@ impl LivingEntity {
// TODO: Falling anvil/stalactite should only damage the helmet slot.
// TODO: Implement DAMAGE_RESISTANT component checks (e.g. netherite vs fire).
for (slot_index, slot) in self.equipment_slots.iter() {
if !slot.is_armor_slot() {
continue;
}
let armor_slots: Vec<(usize, Arc<Mutex<ItemStack>>, EquipmentSlot)> = {
let equipment_lock = self.entity_equipment.lock().await;
self.equipment_slots
.iter()
.filter(|(_, slot)| slot.is_armor_slot())
.map(|(index, slot)| (*index, equipment_lock.get(slot), slot.clone()))
.collect()
};
let equipment = self.entity_equipment.lock().await.get(slot);
for (slot_index, equipment, slot) in armor_slots {
let (slot_result, updated_stack_opt) = {
let mut stack = equipment.lock().await;
if stack.is_empty() {
@ -1568,14 +1578,14 @@ impl LivingEntity {
if slot_result == pumpkin_data::item_stack::DamageResult::Broken {
let world = self.entity.world.load();
world
.send_entity_status(&self.entity, super::equipment_break_status(slot))
.send_entity_status(&self.entity, super::equipment_break_status(&slot))
.await;
}
equipment_updates.push((slot.clone(), updated_stack.clone()));
if let Some(player) = caller.get_player() {
player
.enqueue_slot_set_packet(&CSetPlayerInventory::new(
(*slot_index as i32).into(),
(slot_index as i32).into(),
&ItemStackSerializer::from(updated_stack),
))
.await;

View File

@ -334,14 +334,17 @@ impl EndermanEntity {
}
pub async fn is_player_staring(&self, player: &Player) -> bool {
let equipment = player.living_entity.entity_equipment.lock().await;
let equipment = player.living_entity.entity_equipment.try_lock();
if let Ok(equipment) = equipment {
let head_item = equipment.get(&EquipmentSlot::HEAD);
let head_stack = head_item.lock().await;
if !head_stack.is_empty() && head_stack.item == &Item::CARVED_PUMPKIN {
let head_stack = head_item.try_lock();
if let Ok(head_stack) = head_stack
&& !head_stack.is_empty()
&& head_stack.item == &Item::CARVED_PUMPKIN
{
return false;
}
drop(head_stack);
drop(equipment);
}
let entity = &self.mob_entity.living_entity.entity;
let enderman_pos = entity.pos.load();

View File

@ -355,7 +355,7 @@ impl<T: Mob + Send + 'static> EntityBase for T {
drop(navigator);
let mut look_control = mob_entity.look_control.lock().await;
look_control.tick(self).await;
look_control.tick(self);
drop(look_control);
mob_entity.living_entity.tick(caller, server).await;

View File

@ -44,7 +44,8 @@ use pumpkin_inventory::player::{
player_inventory::PlayerInventory, player_screen_handler::PlayerScreenHandler,
};
use pumpkin_inventory::screen_handler::{
InventoryPlayer, PlayerFuture, ScreenHandler, ScreenHandlerFactory, ScreenHandlerListener,
ClickType, InventoryPlayer, PlayerFuture, ScreenHandler, ScreenHandlerFactory,
ScreenHandlerListener,
};
use pumpkin_inventory::sync_handler::SyncHandler;
use pumpkin_macros::send_cancellable;
@ -63,7 +64,7 @@ use pumpkin_protocol::java::client::play::{
CTitleAnimation, CTitleText, CUnloadChunk, CUpdateMobEffect, CUpdateTime, GameEvent, Metadata,
PlayerAction, PlayerInfoFlags, PreviousMessage,
};
use pumpkin_protocol::java::server::play::SClickSlot;
use pumpkin_protocol::java::server::play::{SClickSlot, SlotActionType};
use pumpkin_util::math::{
boundingbox::BoundingBox, experience, position::BlockPos, vector2::Vector2, vector3::Vector3,
};
@ -87,6 +88,8 @@ use crate::entity::{EntityBaseFuture, NbtFuture, TeleportFuture};
use crate::net::{ClientPlatform, GameProfile};
use crate::net::{DisconnectReason, PlayerConfig};
use crate::plugin::player::exp_change::PlayerExpChangeEvent;
use crate::plugin::player::inventory_close::InventoryCloseEvent;
use crate::plugin::player::inventory_interact::InventoryClickEvent;
use crate::plugin::player::player_change_world::PlayerChangeWorldEvent;
use crate::plugin::player::player_gamemode_change::PlayerGamemodeChangeEvent;
use crate::plugin::player::player_permission_check::PlayerPermissionCheckEvent;
@ -463,9 +466,14 @@ pub struct Player {
pub tab_list_order: AtomicI32,
pub tab_list_latency: AtomicI32,
pub tab_list_listed: AtomicBool,
pub this: Weak<Self>,
}
impl Player {
pub fn this(&self) -> Arc<Self> {
self.this.upgrade().expect("Player should have a reference")
}
#[expect(clippy::too_many_lines)]
pub async fn new(
client: ClientPlatform,
@ -594,7 +602,8 @@ impl Player {
display_name: Mutex::new(None),
tab_list_order: AtomicI32::new(0),
tab_list_latency: AtomicI32::new(0),
tab_list_listed: AtomicBool::new(true),
tab_list_listed: AtomicBool::new(false),
this: Weak::new(),
}
}
@ -2888,13 +2897,20 @@ impl Player {
}
pub async fn on_handled_screen_closed(&self) {
self.current_screen_handler
.lock()
.await
.lock()
.await
.on_closed(self)
let window_type = {
let handler_lock = self.current_screen_handler.lock().await;
let mut handler = handler_lock.lock().await;
let wt = handler.window_type();
handler.on_closed(self).await;
wt
};
if let Some(server) = self.living_entity.entity.world.load().server.upgrade() {
server
.plugin_manager
.fire(InventoryCloseEvent::new(&self.this(), window_type))
.await;
}
let player_screen_handler: Arc<Mutex<dyn ScreenHandler>> =
self.player_screen_handler.clone();
@ -3009,14 +3025,23 @@ impl Player {
self.open_container_pos.store(None);
}
pub async fn on_slot_click(&self, packet: SClickSlot) {
#[allow(clippy::too_many_lines)]
pub async fn on_slot_click(&self, packet: SClickSlot, server: &Server) {
self.update_last_action_time();
let screen_handler = self.current_screen_handler.lock().await;
let mut screen_handler = screen_handler.lock().await;
let behaviour = screen_handler.get_behaviour();
let screen_handler_arc = self.current_screen_handler.lock().await.clone();
let mut screen_handler = screen_handler_arc.lock().await;
// behaviour is dropped here
if i32::from(behaviour.sync_id) != packet.sync_id.0 {
let (sync_id, container_slots, allow_grab_items, allow_put_items) = {
let b = screen_handler.get_behaviour();
(
b.sync_id,
b.container_slots,
b.allow_grab_items,
b.allow_put_items,
)
};
if i32::from(sync_id) != packet.sync_id.0 {
return;
}
@ -3046,7 +3071,146 @@ impl Player {
return;
}
let not_in_sync = packet.revision.0 != (behaviour.revision.load(Ordering::Relaxed) as i32);
// Fire InventoryClickEvent
let clicked_item = if slot >= 0 {
let slot_obj = &screen_handler.get_behaviour().slots[slot as usize];
Some(slot_obj.get_cloned_stack().await)
} else {
None
};
let cursor_item = Some(
screen_handler
.get_behaviour()
.cursor_stack
.lock()
.await
.clone(),
);
let raw_slot = slot; // For now raw_slot == slot, as we don't have separate view/inventory indexing yet
let hotbar_button = if matches!(packet.mode, SlotActionType::Swap) {
packet.button
} else {
-1
};
let click_type = match packet.mode {
SlotActionType::Pickup => {
if packet.button == 0 {
ClickType::Left
} else {
ClickType::Right
}
}
SlotActionType::QuickMove => {
if packet.button == 0 {
ClickType::ShiftLeft
} else {
ClickType::ShiftRight
}
}
SlotActionType::Swap => ClickType::NumberKey(packet.button as u8),
SlotActionType::Clone => ClickType::Middle,
SlotActionType::Throw => {
if packet.button == 0 {
ClickType::Drop
} else {
ClickType::ControlDrop
}
}
SlotActionType::QuickCraft => {
if [0, 4, 8].contains(&packet.button) {
ClickType::Left
} else if [1, 5, 9].contains(&packet.button) {
ClickType::Right
} else {
ClickType::Middle
}
}
SlotActionType::PickupAll => ClickType::DoubleClick,
};
send_cancellable! {{
server;
InventoryClickEvent::new(
&self.this(),
screen_handler.window_type(),
click_type,
slot,
raw_slot,
clicked_item,
cursor_item,
i32::from(hotbar_button),
);
'after: {}
'cancelled: {
screen_handler.cancel(self).await;
return;
}
}}
// Enforce flags
let is_container_slot = slot >= 0 && i32::from(slot) < container_slots as i32;
match packet.mode {
SlotActionType::Pickup => {
let cursor_stack = screen_handler.get_behaviour().cursor_stack.lock().await;
if is_container_slot {
if !cursor_stack.is_empty() && !allow_put_items {
drop(cursor_stack);
screen_handler.cancel(self).await;
return;
}
if cursor_stack.is_empty() && !allow_grab_items {
drop(cursor_stack);
screen_handler.cancel(self).await;
return;
}
}
}
SlotActionType::QuickMove => {
if is_container_slot && !allow_grab_items {
screen_handler.cancel(self).await;
return;
}
if !is_container_slot && !allow_put_items {
screen_handler.cancel(self).await;
return;
}
}
SlotActionType::Swap => {
if is_container_slot && (!allow_grab_items || !allow_put_items) {
screen_handler.cancel(self).await;
return;
}
}
SlotActionType::Throw => {
if is_container_slot && !allow_grab_items {
screen_handler.cancel(self).await;
return;
}
}
SlotActionType::QuickCraft => {
if !allow_put_items {
// Dragging items into slots
screen_handler.cancel(self).await;
return;
}
}
SlotActionType::PickupAll => {
if !allow_grab_items {
screen_handler.cancel(self).await;
return;
}
}
SlotActionType::Clone => {}
}
let not_in_sync = packet.revision.0
!= (screen_handler
.get_behaviour()
.revision
.load(Ordering::Relaxed) as i32);
screen_handler.disable_sync();
screen_handler
@ -3069,7 +3233,6 @@ impl Player {
screen_handler.update_to_client().await;
} else {
screen_handler.send_content_updates().await;
drop(screen_handler);
}
}

View File

@ -54,10 +54,17 @@ impl ItemBehaviour for EggItem {
// Consume item
let held_item = player.inventory.held_item();
let consumed = {
let mut main_hand = held_item.lock().await;
if !main_hand.is_empty() && Self::ids().contains(&main_hand.item.id) {
main_hand.decrement_unless_creative(player.gamemode.load(), 1);
true
} else {
false
}
};
if !consumed {
let off_hand_item = player.inventory.off_hand_item().await;
let mut off_hand = off_hand_item.lock().await;
if !off_hand.is_empty() && Self::ids().contains(&off_hand.item.id) {

View File

@ -52,10 +52,17 @@ impl ItemBehaviour for EnderPearlItem {
// Consume item
let held_item = player.inventory.held_item();
let consumed = {
let mut main_hand = held_item.lock().await;
if !main_hand.is_empty() && main_hand.item.id == Item::ENDER_PEARL.id {
main_hand.decrement_unless_creative(player.gamemode.load(), 1);
true
} else {
false
}
};
if !consumed {
let off_hand_item = player.inventory.off_hand_item().await;
let mut off_hand = off_hand_item.lock().await;
if !off_hand.is_empty() && off_hand.item.id == Item::ENDER_PEARL.id {

View File

@ -9,6 +9,7 @@ use crate::entity::projectile::{
use crate::item::{ItemBehaviour, ItemMetadata};
use pumpkin_data::entity::EntityType;
use pumpkin_data::item::Item;
use pumpkin_data::item_stack::ItemStack;
use pumpkin_data::sound::Sound;
pub struct PotionItem;
@ -72,17 +73,20 @@ impl ItemBehaviour for SplashPotionItem {
// Copy the held item stack data into the projectile
let main = player.inventory.held_item();
let mut used_main = true;
let mut stack = main.lock().await.clone();
if stack.is_empty() || stack.item.id != pumpkin_data::item::Item::SPLASH_POTION.id {
let mut stack = {
let s = main.lock().await.clone();
(!s.is_empty() && s.item.id == pumpkin_data::item::Item::SPLASH_POTION.id)
.then_some(s)
};
if stack.is_none() {
let off = player.inventory.off_hand_item().await;
let off_stack = off.lock().await.clone();
if !off_stack.is_empty()
&& off_stack.item.id == pumpkin_data::item::Item::SPLASH_POTION.id
{
stack = off_stack;
let s = off.lock().await.clone();
if !s.is_empty() && s.item.id == pumpkin_data::item::Item::SPLASH_POTION.id {
stack = Some(s);
used_main = false;
}
}
let stack = stack.unwrap_or_else(|| ItemStack::EMPTY.clone());
splash.set_item_stack(stack).await;
let yaw = player.living_entity.entity.yaw.load();
@ -145,17 +149,20 @@ impl ItemBehaviour for LingeringPotionItem {
// Copy the held item stack data into the projectile
let main = player.inventory.held_item();
let mut used_main = true;
let mut stack = main.lock().await.clone();
if stack.is_empty() || stack.item.id != pumpkin_data::item::Item::LINGERING_POTION.id {
let mut stack = {
let s = main.lock().await.clone();
(!s.is_empty() && s.item.id == pumpkin_data::item::Item::LINGERING_POTION.id)
.then_some(s)
};
if stack.is_none() {
let off = player.inventory.off_hand_item().await;
let off_stack = off.lock().await.clone();
if !off_stack.is_empty()
&& off_stack.item.id == pumpkin_data::item::Item::LINGERING_POTION.id
{
stack = off_stack;
let s = off.lock().await.clone();
if !s.is_empty() && s.item.id == pumpkin_data::item::Item::LINGERING_POTION.id {
stack = Some(s);
used_main = false;
}
}
let stack = stack.unwrap_or_else(|| ItemStack::EMPTY.clone());
ling.set_item_stack(stack).await;
let yaw = player.living_entity.entity.yaw.load();

View File

@ -51,10 +51,17 @@ impl ItemBehaviour for SnowBallItem {
// Consume item
let held_item = player.inventory.held_item();
let consumed = {
let mut main_hand = held_item.lock().await;
if !main_hand.is_empty() && main_hand.item.id == Item::SNOWBALL.id {
main_hand.decrement_unless_creative(player.gamemode.load(), 1);
true
} else {
false
}
};
if !consumed {
let off_hand_item = player.inventory.off_hand_item().await;
let mut off_hand = off_hand_item.lock().await;
if !off_hand.is_empty() && off_hand.item.id == Item::SNOWBALL.id {

View File

@ -804,7 +804,7 @@ impl JavaClient {
}
id if id == SClickSlot::to_id(version) => {
player
.on_slot_click(SClickSlot::read(payload, &version)?)
.on_slot_click(SClickSlot::read(payload, &version)?, server)
.await;
}
id if id == SSetHeldItem::to_id(version) => {

View File

@ -215,9 +215,11 @@ impl JavaClient {
*awaiting_teleport = None;
drop(awaiting_teleport);
} else {
drop(awaiting_teleport);
self.kick(TextComponent::text("Wrong teleport id")).await;
}
} else {
drop(awaiting_teleport);
self.kick(TextComponent::text(
"Send Teleport confirm, but we did not teleport",
))
@ -1005,8 +1007,16 @@ impl JavaClient {
})
.count();
let (grid_width, crafting_inv) = {
let screen_handler_arc = player.current_screen_handler.lock().await.clone();
let mut handler = screen_handler_arc.lock().await;
let handler = screen_handler_arc.lock().await;
let grid_width: usize = match handler.window_type() {
Some(WindowType::Crafting) => 3,
None => 2, // player inventory 2x2
_ => return,
};
(grid_width, handler.get_behaviour().slots[1].get_inventory())
};
if target_id < crafting_display_count {
// Crafting recipe
@ -1025,11 +1035,6 @@ impl JavaClient {
});
let Some(recipe) = recipe else { return };
let grid_width: usize = match handler.window_type() {
Some(WindowType::Crafting) => 3,
None => 2, // player inventory 2x2
_ => return,
};
let grid_size = grid_width * grid_width;
// Map each grid position to its required ingredient (None = empty/unused).
@ -1061,8 +1066,6 @@ impl JavaClient {
_ => return,
}
let crafting_inv = handler.get_behaviour().slots[1].get_inventory();
// Check if this exact recipe is already placed (determines stacking vs fresh fill).
let recipe_matches = {
let mut ok = true;
@ -1124,7 +1127,8 @@ impl JavaClient {
};
if amount_to_craft == 0 {
handler.send_content_updates().await;
let screen_handler_arc = player.current_screen_handler.lock().await.clone();
screen_handler_arc.lock().await.send_content_updates().await;
return;
}
@ -1143,10 +1147,16 @@ impl JavaClient {
return;
};
let furnace_inv = {
let screen_handler_arc = player.current_screen_handler.lock().await.clone();
let handler = screen_handler_arc.lock().await;
match handler.window_type() {
Some(WindowType::Furnace | WindowType::BlastFurnace | WindowType::Smoker) => {}
Some(WindowType::Furnace | WindowType::BlastFurnace | WindowType::Smoker) => {
handler.get_behaviour().slots[0].get_inventory()
}
_ => return,
}
};
let ingredient = match recipe {
CookingRecipeType::Smelting(r)
@ -1155,8 +1165,6 @@ impl JavaClient {
| CookingRecipeType::CampfireCooking(r) => &r.ingredient,
};
let furnace_inv = handler.get_behaviour().slots[0].get_inventory();
// Check if ingredient already matches (for stacking).
let (recipe_matches, current_count) = {
let slot_arc = furnace_inv.get_stack(0).await;
@ -1188,7 +1196,8 @@ impl JavaClient {
}
}
handler.send_content_updates().await;
let screen_handler_arc = player.current_screen_handler.lock().await.clone();
screen_handler_arc.lock().await.send_content_updates().await;
}
pub async fn handle_swing_arm(&self, player: &Arc<Player>, swing_arm: SSwingArm) {
player.update_last_action_time();
@ -1546,15 +1555,17 @@ impl JavaClient {
}
player.world().clone().respawn_player(player, false).await;
{
let screen_handler = player.current_screen_handler.lock().await;
let mut screen_handler = screen_handler.lock().await;
screen_handler.sync_state().await;
drop(screen_handler);
};
// Restore abilities based on gamemode after respawn
{
let mut abilities = player.abilities.lock().await;
abilities.set_for_gamemode(player.gamemode.load());
drop(abilities);
};
player.send_abilities_update().await;
}
1 => {
@ -1967,19 +1978,22 @@ impl JavaClient {
player_abilities: SPlayerAbilities,
server: &Server,
) {
let mut abilities = player.abilities.lock().await;
let (flying, allow_flying) = {
let abilities = player.abilities.lock().await;
(abilities.flying, abilities.allow_flying)
};
// Set the flying ability
let flying = player_abilities.flags & 0x02 != 0 && abilities.allow_flying;
if abilities.flying != flying {
let new_flying = player_abilities.flags & 0x02 != 0 && allow_flying;
if flying != new_flying {
send_cancellable! {{
server;
PlayerToggleFlightEvent::new(player.clone(), flying);
PlayerToggleFlightEvent::new(player.clone(), new_flying);
'after: {
if event.is_flying {
player.living_entity.fall_distance.store(0.0);
}
abilities.flying = event.is_flying;
player.abilities.lock().await.flying = event.is_flying;
}
'cancelled: {
player.send_abilities_update().await;

View File

@ -0,0 +1,42 @@
use std::sync::Arc;
use crate::entity::player::Player;
use pumpkin_data::screen::WindowType;
use pumpkin_macros::Event;
use super::PlayerEvent;
/// Event that is triggered when a player closes an inventory.
#[derive(Event, Clone)]
pub struct InventoryCloseEvent {
/// The player who closed the inventory.
pub player: Arc<Player>,
/// The window type of the inventory that was closed.
pub window_type: Option<WindowType>,
}
impl InventoryCloseEvent {
/// Creates a new instance of `InventoryCloseEvent`.
///
/// # Arguments
///
/// - `player`: A reference-counted pointer to the player who triggered the event.
/// - `window_type`: The window type of the inventory.
///
/// # Returns
///
/// A new `InventoryCloseEvent` instance with the specified data.
pub fn new(player: &Arc<Player>, window_type: Option<WindowType>) -> Self {
Self {
player: Arc::clone(player),
window_type,
}
}
}
impl PlayerEvent for InventoryCloseEvent {
fn get_player(&self) -> &Arc<Player> {
&self.player
}
}

View File

@ -0,0 +1,86 @@
use std::sync::Arc;
use crate::entity::player::Player;
use pumpkin_data::{item_stack::ItemStack, screen::WindowType};
use pumpkin_inventory::screen_handler::ClickType;
use pumpkin_macros::{Event, cancellable};
use super::PlayerEvent;
/// Event that is triggered when a player clicks in an inventory.
#[cancellable]
#[derive(Event, Clone)]
pub struct InventoryClickEvent {
/// The player who performed the interaction.
pub player: Arc<Player>,
/// The window type of the inventory being interacted with.
pub window_type: Option<WindowType>,
/// The type of click performed.
pub click_type: ClickType,
/// The slot index that was clicked.
pub slot: i16,
/// The raw slot number clicked, ready for passing to #getItem(int)
/// This slot number is unique for the view.
pub raw_slot: i16,
/// The item stack that was in the slot being clicked.
pub clicked_item: Option<ItemStack>,
/// The current `ItemStack` on the cursor.
pub cursor: Option<ItemStack>,
/// If the `ClickType` is `NUMBER_KEY`, this field will return the index of the pressed key (0-8).
pub hotbar_button: i32,
}
impl InventoryClickEvent {
/// Creates a new instance of `InventoryClickEvent`.
///
/// # Arguments
///
/// - `player`: A reference-counted pointer to the player who triggered the event.
/// - `window_type`: The window type of the inventory.
/// - `click_type`: The type of click.
/// - `slot`: The slot index that was clicked.
/// - `raw_slot`: The raw slot index.
/// - `clicked_item`: The item stack that was in the slot being clicked.
/// - `cursor`: The item stack on the cursor.
/// - `hotbar_button`: The hotbar button pressed (0-8).
///
/// # Returns
///
/// A new `InventoryClickEvent` instance with the specified data.
#[allow(clippy::too_many_arguments)]
pub fn new(
player: &Arc<Player>,
window_type: Option<WindowType>,
click_type: ClickType,
slot: i16,
raw_slot: i16,
clicked_item: Option<ItemStack>,
cursor: Option<ItemStack>,
hotbar_button: i32,
) -> Self {
Self {
player: Arc::clone(player),
window_type,
click_type,
slot,
raw_slot,
clicked_item,
cursor,
hotbar_button,
cancelled: false,
}
}
}
impl PlayerEvent for InventoryClickEvent {
fn get_player(&self) -> &Arc<Player> {
&self.player
}
}

View File

@ -2,6 +2,8 @@ pub mod changed_main_hand;
pub mod egg_throw;
pub mod exp_change;
pub mod fish;
pub mod inventory_close;
pub mod inventory_interact;
pub mod item_held;
pub mod player_change_world;
pub mod player_chat;

View File

@ -14,6 +14,8 @@ pub struct PluginGui {
pub window_type: WindowType,
pub title: TextComponent,
pub inventory: Arc<PluginInventory>,
pub allow_grab_items: bool,
pub allow_put_items: bool,
}
pub struct PluginInventory {
@ -81,6 +83,14 @@ impl Inventory for PluginInventory {
})
}
fn on_open(&self) -> InventoryFuture<'_, ()> {
Box::pin(async move {})
}
fn on_close(&self) -> InventoryFuture<'_, ()> {
Box::pin(async move {})
}
fn as_any(&self) -> &dyn Any {
self
}
@ -93,10 +103,21 @@ pub struct PluginScreenHandler {
impl PluginScreenHandler {
#[must_use]
pub fn new(sync_id: u8, window_type: WindowType, inventory: &Arc<PluginInventory>) -> Self {
pub fn new(
sync_id: u8,
window_type: WindowType,
inventory: &Arc<PluginInventory>,
allow_grab_items: bool,
allow_put_items: bool,
) -> Self {
let mut behaviour = ScreenHandlerBehaviour::new(sync_id, Some(window_type));
behaviour.allow_grab_items = allow_grab_items;
behaviour.allow_put_items = allow_put_items;
behaviour.container_slots = inventory.size();
let mut handler = Self {
inventory: inventory.clone(),
behaviour: ScreenHandlerBehaviour::new(sync_id, Some(window_type)),
behaviour,
};
for i in 0..inventory.size() {
@ -111,6 +132,7 @@ impl ScreenHandler for PluginScreenHandler {
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;
})
}

View File

@ -1,6 +1,7 @@
use std::sync::Arc;
use pumpkin_data::{Block, entity::EntityType};
use pumpkin_inventory::screen_handler::ClickType;
use pumpkin_protocol::java::server::play::ActionType;
use pumpkin_util::{
GameMode, Hand,
@ -137,6 +138,36 @@ pub(super) const fn from_wasm_game_mode(game_mode: pumpkin::plugin::common::Game
}
}
pub(super) const fn to_wasm_click_type(click_type: ClickType) -> pumpkin::plugin::gui::ClickType {
match click_type {
ClickType::Left => pumpkin::plugin::gui::ClickType::Left,
ClickType::Right => pumpkin::plugin::gui::ClickType::Right,
ClickType::ShiftLeft => pumpkin::plugin::gui::ClickType::ShiftLeft,
ClickType::ShiftRight => pumpkin::plugin::gui::ClickType::ShiftRight,
ClickType::Middle => pumpkin::plugin::gui::ClickType::Middle,
ClickType::Drop => pumpkin::plugin::gui::ClickType::Drop,
ClickType::ControlDrop => pumpkin::plugin::gui::ClickType::ControlDrop,
ClickType::DoubleClick => pumpkin::plugin::gui::ClickType::DoubleClick,
ClickType::NumberKey(_) => pumpkin::plugin::gui::ClickType::NumberKey,
ClickType::Unknown => pumpkin::plugin::gui::ClickType::Unknown,
}
}
pub(super) const fn from_wasm_click_type(click_type: pumpkin::plugin::gui::ClickType) -> ClickType {
match click_type {
pumpkin::plugin::gui::ClickType::Left => ClickType::Left,
pumpkin::plugin::gui::ClickType::Right => ClickType::Right,
pumpkin::plugin::gui::ClickType::ShiftLeft => ClickType::ShiftLeft,
pumpkin::plugin::gui::ClickType::ShiftRight => ClickType::ShiftRight,
pumpkin::plugin::gui::ClickType::Middle => ClickType::Middle,
pumpkin::plugin::gui::ClickType::Drop => ClickType::Drop,
pumpkin::plugin::gui::ClickType::ControlDrop => ClickType::ControlDrop,
pumpkin::plugin::gui::ClickType::DoubleClick => ClickType::DoubleClick,
pumpkin::plugin::gui::ClickType::NumberKey => ClickType::NumberKey(0), // Default to 0
pumpkin::plugin::gui::ClickType::Unknown => ClickType::Unknown,
}
}
pub(super) fn consume_player(
state: &mut PluginHostState,
player: &Resource<pumpkin::plugin::player::Player>,

View File

@ -4,13 +4,15 @@ use crate::plugin::{
wit::v0_1::{
events::{
ToFromWasmEvent, consume_player, consume_text_component, consume_world,
from_wasm_entity_interaction_action, from_wasm_entity_type, from_wasm_game_mode,
from_wasm_hand, from_wasm_position, to_wasm_block_position,
to_wasm_entity_interaction_action, to_wasm_entity_type, to_wasm_game_mode,
to_wasm_hand, to_wasm_position,
from_wasm_click_type, from_wasm_entity_interaction_action, from_wasm_entity_type,
from_wasm_game_mode, from_wasm_hand, from_wasm_position, to_wasm_block_position,
to_wasm_click_type, to_wasm_entity_interaction_action, to_wasm_entity_type,
to_wasm_game_mode, to_wasm_hand, to_wasm_position,
},
player::to_wit_item_stack,
pumpkin::plugin::event::{
Event, InteractAction as WasmInteractAction, PlayerChangeWorldEventData,
Event, InteractAction as WasmInteractAction, InventoryClickEventData,
InventoryCloseEventData, PlayerChangeWorldEventData,
PlayerChangedMainHandEventData, PlayerChatEventData, PlayerCommandSendEventData,
PlayerCustomPayloadEventData, PlayerEggThrowEventData, PlayerExpChangeEventData,
PlayerFishEventData, PlayerFishState as WasmPlayerFishState,
@ -28,6 +30,8 @@ use crate::plugin::{
egg_throw::PlayerEggThrowEvent,
exp_change::PlayerExpChangeEvent,
fish::{PlayerFishEvent, PlayerFishState},
inventory_close::InventoryCloseEvent,
inventory_interact::InventoryClickEvent,
item_held::PlayerItemHeldEvent,
player_change_world::PlayerChangeWorldEvent,
player_chat::PlayerChatEvent,
@ -73,6 +77,66 @@ const fn from_wasm_fish_state(state: WasmPlayerFishState) -> PlayerFishState {
}
}
impl ToFromWasmEvent for InventoryCloseEvent {
fn to_wasm_event(&self, state: &mut PluginHostState) -> Event {
let player = state
.add_player(self.player.clone())
.expect("failed to add player resource");
Event::InventoryCloseEvent(InventoryCloseEventData {
player,
window_type: self.window_type.map(|wt| format!("{wt:?}")),
})
}
fn from_wasm_event(event: Event, state: &mut PluginHostState) -> Self {
match event {
Event::InventoryCloseEvent(data) => Self {
player: consume_player(state, &data.player),
window_type: None, // We don't change window_type from WASM
},
_ => panic!("unexpected event type"),
}
}
}
impl ToFromWasmEvent for InventoryClickEvent {
fn to_wasm_event(&self, state: &mut PluginHostState) -> Event {
let player = state
.add_player(self.player.clone())
.expect("failed to add player resource");
Event::InventoryClickEvent(InventoryClickEventData {
player,
window_type: self.window_type.map(|wt| format!("{wt:?}")),
click_type: to_wasm_click_type(self.click_type),
slot: self.slot,
raw_slot: self.raw_slot,
clicked_item: self.clicked_item.as_ref().and_then(to_wit_item_stack),
cursor: self.cursor.as_ref().and_then(to_wit_item_stack),
hotbar_button: self.hotbar_button,
cancelled: self.cancelled,
})
}
fn from_wasm_event(event: Event, state: &mut PluginHostState) -> Self {
match event {
Event::InventoryClickEvent(data) => Self {
player: consume_player(state, &data.player),
window_type: None, // We don't change window_type from WASM
click_type: from_wasm_click_type(data.click_type),
slot: data.slot,
raw_slot: data.raw_slot,
clicked_item: None, // We don't change clicked_item from WASM
cursor: None, // We don't change cursor from WASM
hotbar_button: data.hotbar_button,
cancelled: data.cancelled,
},
_ => panic!("unexpected event type"),
}
}
}
impl ToFromWasmEvent for PlayerJoinEvent {
fn to_wasm_event(&self, state: &mut PluginHostState) -> Event {
let player = state

View File

@ -74,6 +74,8 @@ impl gui::HostGui for PluginHostState {
window_type,
title,
inventory: Arc::new(PluginInventory::new(size)),
allow_grab_items: true,
allow_put_items: true,
}));
self.add_gui(gui)
@ -177,6 +179,36 @@ impl gui::HostGui for PluginHostState {
Ok(())
}
async fn set_allow_grab_items(
&mut self,
res: Resource<Gui>,
allow: bool,
) -> wasmtime::Result<()> {
let mut gui = self.get_gui_res(&res)?.provider.lock().await;
gui.allow_grab_items = allow;
Ok(())
}
async fn get_allow_grab_items(&mut self, res: Resource<Gui>) -> wasmtime::Result<bool> {
let gui = self.get_gui_res(&res)?.provider.lock().await;
Ok(gui.allow_grab_items)
}
async fn set_allow_put_items(
&mut self,
res: Resource<Gui>,
allow: bool,
) -> wasmtime::Result<()> {
let mut gui = self.get_gui_res(&res)?.provider.lock().await;
gui.allow_put_items = allow;
Ok(())
}
async fn get_allow_put_items(&mut self, res: Resource<Gui>) -> wasmtime::Result<bool> {
let gui = self.get_gui_res(&res)?.provider.lock().await;
Ok(gui.allow_put_items)
}
async fn drop(&mut self, rep: Resource<Gui>) -> wasmtime::Result<()> {
self.resource_table
.delete::<GuiResource>(Resource::new_own(rep.rep()))

View File

@ -426,6 +426,8 @@ impl pumpkin::plugin::player::HostPlayer for PluginHostState {
sync_id,
gui.window_type,
&gui.inventory,
gui.allow_grab_items,
gui.allow_put_items,
)));
player

View File

@ -430,7 +430,8 @@ impl Server {
}
// Wrap in Arc after data is loaded
let player = Arc::new(player);
let mut player = Arc::new(player);
Arc::get_mut(&mut player).unwrap().this = Arc::downgrade(&player);
send_cancellable! {{
self;

View File

@ -6,6 +6,7 @@
use std::sync::Arc;
use tokio::sync::Mutex;
use tracing::{debug, info};
use uuid::Uuid;
@ -131,45 +132,67 @@ impl DragonFight {
// ── Main tick ─────────────────────────────────────────────────────────────
pub async fn tick(&mut self, world: &Arc<World>) {
pub async fn tick(fight_mutex: &Mutex<Self>, world: &Arc<World>) {
let (
ticks_since_last_player_scan,
needs_state_scanning,
respawn_stage,
dragon_killed,
dragon_uuid,
) = {
let mut fight = fight_mutex.lock().await;
fight.ticks_since_last_player_scan += 1;
(
fight.ticks_since_last_player_scan,
fight.needs_state_scanning,
fight.respawn_stage,
fight.dragon_killed,
fight.dragon_uuid,
)
};
// 1. Update boss-bar recipients every 20 ticks.
self.ticks_since_last_player_scan += 1;
if self.ticks_since_last_player_scan >= PLAYER_SCAN_INTERVAL {
self.update_players(world).await;
self.ticks_since_last_player_scan = 0;
if ticks_since_last_player_scan >= PLAYER_SCAN_INTERVAL {
let mut fight = fight_mutex.lock().await;
fight.update_players(world).await;
fight.ticks_since_last_player_scan = 0;
}
let is_empty = { fight_mutex.lock().await.bossbar_players.is_empty() };
// Nothing to do without nearby players.
if self.bossbar_players.is_empty() {
if is_empty {
return;
}
// 2. One-time state scan on the first populated tick.
if self.needs_state_scanning {
self.scan_state(world).await;
self.needs_state_scanning = false;
if needs_state_scanning {
let mut fight = fight_mutex.lock().await;
fight.scan_state(world).await;
fight.needs_state_scanning = false;
}
// 3. Respawn sequence (takes priority over normal dragon-missing logic).
if self.respawn_stage.is_some() {
self.tick_respawn(world).await;
if respawn_stage.is_some() {
let mut fight = fight_mutex.lock().await;
fight.tick_respawn(world).await;
return;
}
// 4. Normal fight ticking.
if !self.dragon_killed {
self.ticks_since_dragon_seen += 1;
if self.dragon_uuid.is_none()
|| self.ticks_since_dragon_seen >= MAX_TICKS_BEFORE_DRAGON_RESPAWN
if !dragon_killed {
let mut fight = fight_mutex.lock().await;
fight.ticks_since_dragon_seen += 1;
if dragon_uuid.is_none()
|| fight.ticks_since_dragon_seen >= MAX_TICKS_BEFORE_DRAGON_RESPAWN
{
self.find_or_create_dragon(world).await;
self.ticks_since_dragon_seen = 0;
fight.find_or_create_dragon(world).await;
fight.ticks_since_dragon_seen = 0;
}
self.ticks_since_crystals_scanned += 1;
if self.ticks_since_crystals_scanned >= CRYSTAL_SCAN_INTERVAL {
self.update_crystal_count(world);
self.ticks_since_crystals_scanned = 0;
fight.ticks_since_crystals_scanned += 1;
if fight.ticks_since_crystals_scanned >= CRYSTAL_SCAN_INTERVAL {
fight.update_crystal_count(world);
fight.ticks_since_crystals_scanned = 0;
}
}
}

View File

@ -135,6 +135,7 @@ use rand::{RngExt, rng};
use scoreboard::Scoreboard;
use time::LevelTime;
use tokio::sync::Mutex;
use tokio::task::JoinSet;
pub mod border;
pub mod bossbar;
@ -792,7 +793,7 @@ impl World {
// Tick the End dragon fight (only on THE_END worlds).
if let Some(ref fight_mutex) = self.dragon_fight {
fight_mutex.lock().await.tick(self).await;
dragon_fight::DragonFight::tick(fight_mutex, self).await;
}
let total_elapsed = start.elapsed();
@ -848,6 +849,7 @@ impl World {
}
async fn tick_environment(&self) {
let (world_age, is_night, time_of_day) = {
let mut level_time = self.level_time.lock().await;
let (advance_time, advance_weather) = {
let lock = self.level_info.load();
@ -878,14 +880,22 @@ impl World {
self.level.level_channel.notify();
}
}
(
level_time.world_age,
level_time.is_night(),
level_time.time_of_day,
)
};
let mut weather = self.weather.lock().await;
weather.tick_weather(self).await;
if self.should_skip_night() && level_time.is_night() {
let time = level_time.time_of_day + 24000;
if self.should_skip_night() && is_night {
let mut level_time = self.level_time.lock().await;
let time = time_of_day + 24000;
level_time.set_time(time - time % 24000);
level_time.send_time(self).await;
drop(level_time);
for player in self.players.load().iter() {
player.wake_up().await;
@ -894,11 +904,13 @@ impl World {
if weather.weather_cycle_enabled && (weather.raining || weather.thundering) {
weather.reset_weather_cycle(self).await;
}
} else if level_time.world_age % 20 == 0 {
} else if world_age % 20 == 0 {
let level_time = self.level_time.lock().await;
level_time.send_time(self).await;
}
}
#[expect(clippy::too_many_lines)]
pub async fn tick_chunks(self: &Arc<Self>) {
let tick_data = self.level.get_tick_data();
for scheduled_tick in tick_data.block_ticks {
@ -974,7 +986,7 @@ impl World {
}
}
let mut spawn_state =
let spawn_state =
SpawnState::new(spawning_chunks_map.len() as i32, &self.entities, self).await; // TODO store it
// TODO gamerule this.spawnEnemies || this.spawnFriendlies
@ -1002,17 +1014,33 @@ impl World {
spawning_chunks_map.into_iter().collect();
spawning_chunks.shuffle(&mut rng());
// TODO i think it can be multithread
let mut spawn_tasks = JoinSet::new();
let spawn_state = Arc::new(spawn_state);
let spawn_list = Arc::new(spawn_list);
for (pos, chunk) in spawning_chunks {
self.tick_spawning_chunk(pos, &chunk, &spawn_list, &mut spawn_state)
let world = self.clone();
let spawn_list = spawn_list.clone();
let spawn_state = spawn_state.clone();
let chunk = chunk.clone();
spawn_tasks.spawn(async move {
world
.tick_spawning_chunk(pos, &chunk, &spawn_list, &spawn_state)
.await;
});
}
while spawn_tasks.join_next().await.is_some() {}
let world: Arc<dyn SimpleWorld> = self.clone();
let mut block_entity_tasks = JoinSet::new();
for block_entity in tick_data.block_entities {
let world = world.clone();
block_entity_tasks.spawn(async move {
block_entity.tick(&world).await;
});
}
while block_entity_tasks.join_next().await.is_some() {}
}
pub async fn get_fluid_collisions(self: &Arc<Self>, bounding_box: BoundingBox) -> Vec<&Fluid> {
@ -1349,12 +1377,16 @@ impl World {
chunk_pos: Vector2<i32>,
chunk: &Arc<ChunkData>,
spawn_list: &Vec<&'static MobCategory>,
spawn_state: &mut SpawnState,
spawn_state: &Arc<SpawnState>,
) {
// this.level.tickThunder(chunk);
//TODO check in simulation distance
let (is_raining, is_thundering) = {
let weather = self.weather.lock().await;
if weather.raining && weather.thundering && rng().random_range(0..100_000) == 0 {
(weather.raining, weather.thundering)
};
if is_raining && is_thundering && rng().random_range(0..100_000) == 0 {
let rand_value = rng().random::<i32>() >> 2;
let delta = Vector3::new(rand_value & 15, rand_value >> 16 & 15, rand_value >> 8 & 15);
let random_pos = Vector3::new(
@ -1395,7 +1427,6 @@ impl World {
self.spawn_entity(Arc::new(entity)).await;
}
}
drop(weather);
if spawn_list.is_empty() {
return;

View File

@ -22,40 +22,60 @@ use pumpkin_world::chunk::io::Dirtiable;
use pumpkin_world::chunk::{ChunkData, ChunkHeightmapType};
use rand::seq::IndexedRandom;
use rand::{RngExt, rng};
use std::collections::HashMap;
use std::collections::hash_map::Entry;
use std::fmt;
use std::sync::Arc;
use uuid::Uuid;
const MAGIC_NUMBER: i32 = 17 * 17;
#[derive(Default, Debug)]
pub struct MobCounts([i32; 8]);
use dashmap::DashMap;
use std::sync::atomic::{AtomicI32, Ordering::Relaxed};
impl MobCounts {
#[inline]
const fn add(&mut self, category: &'static MobCategory) {
self.0[category.id] += 1;
}
#[inline]
const fn can_spawn(&self, category: &'static MobCategory) -> bool {
self.0[category.id] < category.max
pub struct MobCounts([AtomicI32; 8]);
impl Default for MobCounts {
fn default() -> Self {
Self(std::array::from_fn(|_| AtomicI32::new(0)))
}
}
impl fmt::Debug for MobCounts {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_list()
.entries(self.0.iter().map(|a| a.load(Relaxed)))
.finish()
}
}
impl MobCounts {
#[inline]
pub fn add(&self, category: &'static MobCategory) {
self.0[category.id].fetch_add(1, Relaxed);
}
#[inline]
pub fn can_spawn(&self, category: &'static MobCategory) -> bool {
self.0[category.id].load(Relaxed) < category.max
}
}
#[derive(Default)]
pub struct LocalMobCapCalculator {
player_mob_counts: HashMap<i32, MobCounts>,
players_near_chunk: HashMap<Vector2<i32>, Vec<i32>>,
player_mob_counts: DashMap<i32, MobCounts>,
players_near_chunk: DashMap<Vector2<i32>, Vec<i32>>,
}
impl Default for LocalMobCapCalculator {
fn default() -> Self {
Self {
player_mob_counts: DashMap::new(),
players_near_chunk: DashMap::new(),
}
}
}
impl fmt::Debug for LocalMobCapCalculator {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_struct("LocalMobCapCalculator")
.field("world", &"<skipped>")
.field("player_mob_counts", &self.player_mob_counts)
.field("players_near_chunk", &self.players_near_chunk)
.finish()
}
}
@ -67,14 +87,11 @@ impl LocalMobCapCalculator {
dx * dx + dy * dy
}
fn get_players_near<'b>(
players_near_chunk: &'b mut HashMap<Vector2<i32>, Vec<i32>>,
world: &Arc<World>,
chunk_pos: Vector2<i32>,
) -> &'b Vec<i32> {
match players_near_chunk.entry(chunk_pos) {
Entry::Occupied(value) => value.into_mut(),
Entry::Vacant(entry) => {
fn get_players_near(&self, world: &Arc<World>, chunk_pos: Vector2<i32>) -> Vec<i32> {
if let Some(players) = self.players_near_chunk.get(&chunk_pos) {
return players.value().clone();
}
let mut players = Vec::new();
for player in world.players.load().iter() {
if player.gamemode.load() == GameMode::Spectator {
@ -84,37 +101,34 @@ impl LocalMobCapCalculator {
players.push(player.entity_id());
}
}
entry.insert(players)
}
}
self.players_near_chunk.insert(chunk_pos, players.clone());
players
}
pub fn add_mob(
&mut self,
&self,
chunk_pos: Vector2<i32>,
world: &Arc<World>,
category: &'static MobCategory,
) {
let players = Self::get_players_near(&mut self.players_near_chunk, world, chunk_pos);
let players = self.get_players_near(world, chunk_pos);
for player in players {
self.player_mob_counts
.entry(*player)
.entry(player)
.or_default()
.add(category);
}
// debug!("player_mob_counts {:?}", self.player_mob_counts);
// debug!("players_near_chunk {:?}", self.players_near_chunk);
// debug!("chunk_pos {:?}", chunk_pos);
// debug!("players {:?}", players);
}
pub fn can_spawn(
&mut self,
&self,
category: &'static MobCategory,
world: &Arc<World>,
chunk_pos: Vector2<i32>,
) -> bool {
let players = Self::get_players_near(&mut self.players_near_chunk, world, chunk_pos);
let players = self.get_players_near(world, chunk_pos);
for player in players {
if let Some(count) = self.player_mob_counts.get(player) {
if let Some(count) = self.player_mob_counts.get(&player) {
if count.can_spawn(category) {
return true;
}
@ -137,12 +151,15 @@ impl PointCharge {
}
#[derive(Default, Debug)]
struct PotentialCalculator(Vec<PointCharge>);
struct PotentialCalculator(std::sync::Mutex<Vec<PointCharge>>);
impl PotentialCalculator {
pub fn add_charge(&mut self, pos: &BlockPos, charge: f64) {
pub fn add_charge(&self, pos: &BlockPos, charge: f64) {
if charge != 0. {
self.0.push(PointCharge(pos.to_f64(), charge));
self.0
.lock()
.unwrap()
.push(PointCharge(pos.to_f64(), charge));
}
}
pub fn get_potential_energy_change(&self, pos: &BlockPos, charge: f64) -> f64 {
@ -150,22 +167,23 @@ impl PotentialCalculator {
return 0.;
}
let mut sum: f64 = 0.;
for i in &self.0 {
let charges = self.0.lock().unwrap();
for i in charges.iter() {
sum += i.get_potential_change(pos);
}
sum * charge
}
}
use crossbeam::atomic::AtomicCell;
pub struct SpawnState {
spawnable_chunk_count: i32,
pub mob_category_counts: MobCounts,
spawn_potential: PotentialCalculator,
local_mob_cap_calculator: LocalMobCapCalculator,
// unmodifiable_mob_category_counts: MobCounts, seems only for debug
last_checked_pos: BlockPos,
last_checked_type: &'static EntityType,
last_charge: f64,
last_checked: AtomicCell<Option<(BlockPos, &'static EntityType, f64)>>,
}
impl fmt::Debug for SpawnState {
@ -175,9 +193,7 @@ impl fmt::Debug for SpawnState {
.field("mob_category_counts", &self.mob_category_counts)
.field("spawn_potential", &self.spawn_potential)
.field("local_mob_cap_calculator", &self.local_mob_cap_calculator)
.field("last_checked_pos", &self.last_checked_pos)
.field("last_checked_type", &self.last_checked_type.resource_name)
.field("last_charge", &self.last_charge)
.field("last_checked", &self.last_checked)
.finish()
}
}
@ -188,9 +204,9 @@ impl SpawnState {
entities: &ArcSwap<Vec<Arc<dyn EntityBase>>>,
world: &Arc<World>,
) -> Self {
let mut potential = PotentialCalculator::default();
let mut local_mob_cap = LocalMobCapCalculator::default();
let mut counter = MobCounts::default();
let potential = PotentialCalculator::default();
let local_mob_cap = LocalMobCapCalculator::default();
let counter = MobCounts::default();
for entity in entities.load().iter() {
let entity = entity.get_entity();
let entity_type = entity.entity_type;
@ -213,19 +229,16 @@ impl SpawnState {
mob_category_counts: counter,
spawn_potential: potential,
local_mob_cap_calculator: local_mob_cap,
// unmodifiable_mob_category_counts: counter,
last_checked_pos: BlockPos::new(i32::MAX, i32::MAX, i32::MAX),
last_checked_type: &EntityType::PLAYER,
last_charge: 0.,
last_checked: AtomicCell::new(None),
}
}
#[inline]
const fn can_spawn_for_category_global(&self, category: &'static MobCategory) -> bool {
self.mob_category_counts.0[category.id]
pub fn can_spawn_for_category_global(&self, category: &'static MobCategory) -> bool {
self.mob_category_counts.0[category.id].load(Relaxed)
< category.max * self.spawnable_chunk_count / MAGIC_NUMBER
}
fn can_spawn_for_category_local(
&mut self,
pub fn can_spawn_for_category_local(
&self,
world: &Arc<World>,
category: &'static MobCategory,
chunk_pos: Vector2<i32>,
@ -233,44 +246,56 @@ impl SpawnState {
self.local_mob_cap_calculator
.can_spawn(category, world, chunk_pos)
}
async fn can_spawn(
&mut self,
pub async fn can_spawn(
&self,
entity_type: &'static EntityType,
pos: &BlockPos,
world: &Arc<World>,
) -> bool {
self.last_checked_pos = *pos;
self.last_checked_type = entity_type;
// TODO get biome
let biome = world.level.get_rough_biome(pos).await;
if let Some(cost) = biome.spawn_costs.get(entity_type.resource_name) {
self.last_charge = cost.charge;
biome
.spawn_costs
.get(entity_type.resource_name)
.map_or_else(
|| {
self.last_checked.store(Some((*pos, entity_type, 0.)));
true
},
|cost| {
self.last_checked
.store(Some((*pos, entity_type, cost.charge)));
self.spawn_potential
.get_potential_energy_change(pos, cost.charge)
<= cost.energy_budget
} else {
self.last_charge = 0.;
true
},
)
}
}
async fn after_spawn(
&mut self,
pub async fn after_spawn(
&self,
entity_type: &'static EntityType,
pos: &BlockPos,
world: &Arc<World>,
) {
let charge;
if self.last_checked_pos.eq(pos) && self.last_checked_type == entity_type {
charge = self.last_charge;
let charge = if let Some((l_pos, l_type, l_charge)) = self.last_checked.load()
&& l_pos.eq(pos)
&& l_type == entity_type
{
Some(l_charge)
} else {
None
};
let charge = if let Some(charge) = charge {
charge
} else {
// TODO get biome
let biome = world.level.get_rough_biome(pos).await;
if let Some(cost) = biome.spawn_costs.get(entity_type.resource_name) {
charge = cost.charge;
} else {
charge = 0.;
}
}
biome
.spawn_costs
.get(entity_type.resource_name)
.map_or(0., |cost| cost.charge)
};
self.spawn_potential.add_charge(pos, charge);
self.mob_category_counts.add(entity_type.category);
@ -316,7 +341,7 @@ pub async fn spawn_for_chunk(
world: &Arc<World>,
chunk_pos: Vector2<i32>,
chunk: &Arc<ChunkData>,
spawn_state: &mut SpawnState,
spawn_state: &SpawnState,
spawn_list: &Vec<&'static MobCategory>,
) {
// debug!("spawn for chunk {:?}", chunk_pos);
@ -354,7 +379,7 @@ pub async fn spawn_category_for_position(
world: &Arc<World>,
pos: BlockPos,
chunk_pos: &Vector2<i32>,
spawn_state: &mut SpawnState,
spawn_state: &SpawnState,
) {
// TODO StructureManager structureManager = level.structureManager();
// TODO blockState.isRedstoneConductor(chunk, pos) is true then return