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", "bytes",
"console-subscriber", "console-subscriber",
"crossbeam", "crossbeam",
"dashmap",
"flate2", "flate2",
"futures", "futures",
"hmac 0.13.0", "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; mod error;
pub mod furnace_like; pub mod furnace_like;
pub mod generic_container_screen_handler; pub mod generic_container_screen_handler;
pub mod gui_builder;
pub mod player; pub mod player;
pub mod screen_handler; pub mod screen_handler;
pub mod slot; 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. /// Public entry point for slot click handling.
fn on_slot_click<'a>( fn on_slot_click<'a>(
&'a mut self, &'a mut self,
@ -1234,6 +1241,26 @@ pub struct ScreenHandlerBehaviour {
pub window_type: Option<WindowType>, pub window_type: Option<WindowType>,
/// Slots selected during a drag operation (for multi-slot distribution). /// Slots selected during a drag operation (for multi-slot distribution).
pub drag_slots: Vec<u32>, 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 { impl ScreenHandlerBehaviour {
@ -1254,6 +1281,9 @@ impl ScreenHandlerBehaviour {
tracked_property_values: Vec::new(), tracked_property_values: Vec::new(),
window_type, window_type,
drag_slots: Vec::new(), 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, 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. /// Represents the various poses an entity can be in.
enum entity-pose { enum entity-pose {
standing, standing,

View File

@ -1,5 +1,5 @@
interface event { 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 player.{player};
use text.{text-component}; use text.{text-component};
use %world.{%world}; use %world.{%world};
@ -179,6 +179,23 @@ interface event {
cancelled: bool 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 { record block-redstone-event-data {
target-world: %world, target-world: %world,
state-id: u16, state-id: u16,
@ -272,6 +289,8 @@ interface event {
player-toggle-sneak-event, player-toggle-sneak-event,
player-toggle-flight-event, player-toggle-flight-event,
player-toggle-sprint-event, player-toggle-sprint-event,
inventory-click-event,
inventory-close-event,
block-redstone-event, block-redstone-event,
block-break-event, block-break-event,
block-burn-event, block-burn-event,
@ -305,6 +324,8 @@ interface event {
player-toggle-sneak-event(player-toggle-sneak-event-data), player-toggle-sneak-event(player-toggle-sneak-event-data),
player-toggle-flight-event(player-toggle-flight-event-data), player-toggle-flight-event(player-toggle-flight-event-data),
player-toggle-sprint-event(player-toggle-sprint-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-redstone-event(block-redstone-event-data),
block-break-event(block-break-event-data), block-break-event(block-break-event-data),
block-burn-event(block-burn-event-data), block-burn-event(block-burn-event-data),

View File

@ -1,6 +1,6 @@
interface gui { interface gui {
use text.{text-component}; use text.{text-component};
use common.{item-stack}; use common.{item-stack, click-type};
enum gui-type { enum gui-type {
generic-9x1, generic-9x1,
@ -46,5 +46,17 @@ interface gui {
/// Clears all items from the GUI. /// Clears all items from the GUI.
clear-items: func(); 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-protocol.workspace = true
pumpkin-macros.workspace = true pumpkin-macros.workspace = true
dashmap.workspace = true
crossbeam.workspace = true crossbeam.workspace = true
uuid.workspace = true uuid.workspace = true
tokio = { workspace = true, features = [ tokio = { workspace = true, features = [

View File

@ -68,7 +68,7 @@ impl LookControl {
self.look_at_timer = 2; 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(); let entity = mob.get_entity();
if Self::should_stay_horizontal() { if Self::should_stay_horizontal() {
entity.set_pitch(0.0); 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 { const fn should_stay_horizontal() -> bool {
true 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 mob_entity = mob.get_mob_entity();
let navigator = mob_entity.navigator.lock().await; if let Ok(navigator) = mob_entity.navigator.try_lock()
if !navigator.is_idle() { && !navigator.is_idle()
{
let entity = &mob_entity.living_entity.entity; let entity = &mob_entity.living_entity.entity;
let max_head_rotation = mob.get_max_head_rotation(); let max_head_rotation = mob.get_max_head_rotation();
entity.head_yaw.store(clamp_angle( 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> { fn should_continue<'a>(&'a self, mob: &'a dyn Mob) -> GoalFuture<'a, bool> {
Box::pin(async { 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; return false;
}; };
if !target.get_entity().is_alive() { if !target.get_entity().is_alive() {
@ -85,7 +85,12 @@ impl Goal for MeleeAttackGoal {
} }
if !self.pause_when_mob_idle { 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 let is_valid_target = !target
@ -104,7 +109,8 @@ impl Goal for MeleeAttackGoal {
Box::pin(async { Box::pin(async {
// TODO: add missing fields like mob attacking to true and correct Navigation methods // 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 mut navigator = mob.get_mob_entity().navigator.lock().await;
let target_pos = target.get_entity().pos.load(); let target_pos = target.get_entity().pos.load();
navigator.set_progress(NavigatorGoal { navigator.set_progress(NavigatorGoal {
@ -144,8 +150,8 @@ impl Goal for MeleeAttackGoal {
fn tick<'a>(&'a mut self, mob: &'a dyn Mob) -> GoalFuture<'a, ()> { fn tick<'a>(&'a mut self, mob: &'a dyn Mob) -> GoalFuture<'a, ()> {
Box::pin(async { Box::pin(async {
let target_lock = mob.get_mob_entity().target.lock().await; let target = mob.get_mob_entity().target.lock().await.clone();
let Some(target) = target_lock.as_ref() else { let Some(target) = target else {
return; return;
}; };
@ -153,7 +159,7 @@ impl Goal for MeleeAttackGoal {
.look_control .look_control
.lock() .lock()
.await .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); 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_data::attributes::Attributes;
use pumpkin_util::math::wrap_degrees; use pumpkin_util::math::wrap_degrees;
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::atomic::Ordering; use std::sync::atomic::{AtomicBool, Ordering};
pub mod binary_heap; pub mod binary_heap;
pub mod node; pub mod node;
@ -61,6 +61,9 @@ pub struct Navigator {
// Reusable allocations to avoid per-pathfind heap allocations // Reusable allocations to avoid per-pathfind heap allocations
open_set: BinaryHeap, open_set: BinaryHeap,
neighbors_buf: Vec<Node>, 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 { impl Default for Navigator {
@ -79,6 +82,7 @@ impl Default for Navigator {
repath_cooldown: 0, repath_cooldown: 0,
open_set: BinaryHeap::new(), open_set: BinaryHeap::new(),
neighbors_buf: Vec::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 { impl Navigator {
pub fn set_progress(&mut self, goal: NavigatorGoal) { pub fn set_progress(&mut self, goal: NavigatorGoal) {
self.is_idle.store(false, Ordering::Relaxed);
self.current_goal = Some(goal); self.current_goal = Some(goal);
self.current_path = None; self.current_path = None;
} }
@ -106,6 +111,7 @@ impl Navigator {
} }
pub fn stop(&mut self) { pub fn stop(&mut self) {
self.is_idle.store(true, Ordering::Relaxed);
self.current_goal = None; self.current_goal = None;
self.current_path = None; self.current_path = None;
self.ticks_on_current_node = 0; self.ticks_on_current_node = 0;
@ -300,11 +306,13 @@ impl Navigator {
pub async fn tick(&mut self, entity: &LivingEntity) { pub async fn tick(&mut self, entity: &LivingEntity) {
let Some(goal) = self.current_goal.take() else { let Some(goal) = self.current_goal.take() else {
// Idle: stop the mob // Idle: stop the mob
self.is_idle.store(true, Ordering::Relaxed);
entity.movement_input.store(Vector3::new(0.0, 0.0, 0.0)); entity.movement_input.store(Vector3::new(0.0, 0.0, 0.0));
return; return;
}; };
if goal.current_progress == goal.destination { if goal.current_progress == goal.destination {
self.is_idle.store(true, Ordering::Relaxed);
self.current_path = None; self.current_path = None;
entity.movement_input.store(Vector3::new(0.0, 0.0, 0.0)); entity.movement_input.store(Vector3::new(0.0, 0.0, 0.0));
return; return;
@ -437,6 +445,7 @@ impl Navigator {
.store(false, std::sync::atomic::Ordering::SeqCst); .store(false, std::sync::atomic::Ordering::SeqCst);
} }
} else { } else {
self.is_idle.store(true, Ordering::Relaxed);
self.current_path = None; self.current_path = None;
entity.movement_input.store(Vector3::new(0.0, 0.0, 0.0)); entity.movement_input.store(Vector3::new(0.0, 0.0, 0.0));
} }
@ -446,7 +455,7 @@ impl Navigator {
} }
#[must_use] #[must_use]
pub const fn is_idle(&self) -> bool { pub fn is_idle(&self) -> bool {
self.current_goal.is_none() self.is_idle.load(Ordering::Relaxed)
} }
} }

View File

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

View File

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

View File

@ -355,7 +355,7 @@ impl<T: Mob + Send + 'static> EntityBase for T {
drop(navigator); drop(navigator);
let mut look_control = mob_entity.look_control.lock().await; let mut look_control = mob_entity.look_control.lock().await;
look_control.tick(self).await; look_control.tick(self);
drop(look_control); drop(look_control);
mob_entity.living_entity.tick(caller, server).await; 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, player_inventory::PlayerInventory, player_screen_handler::PlayerScreenHandler,
}; };
use pumpkin_inventory::screen_handler::{ use pumpkin_inventory::screen_handler::{
InventoryPlayer, PlayerFuture, ScreenHandler, ScreenHandlerFactory, ScreenHandlerListener, ClickType, InventoryPlayer, PlayerFuture, ScreenHandler, ScreenHandlerFactory,
ScreenHandlerListener,
}; };
use pumpkin_inventory::sync_handler::SyncHandler; use pumpkin_inventory::sync_handler::SyncHandler;
use pumpkin_macros::send_cancellable; use pumpkin_macros::send_cancellable;
@ -63,7 +64,7 @@ use pumpkin_protocol::java::client::play::{
CTitleAnimation, CTitleText, CUnloadChunk, CUpdateMobEffect, CUpdateTime, GameEvent, Metadata, CTitleAnimation, CTitleText, CUnloadChunk, CUpdateMobEffect, CUpdateTime, GameEvent, Metadata,
PlayerAction, PlayerInfoFlags, PreviousMessage, PlayerAction, PlayerInfoFlags, PreviousMessage,
}; };
use pumpkin_protocol::java::server::play::SClickSlot; use pumpkin_protocol::java::server::play::{SClickSlot, SlotActionType};
use pumpkin_util::math::{ use pumpkin_util::math::{
boundingbox::BoundingBox, experience, position::BlockPos, vector2::Vector2, vector3::Vector3, 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::{ClientPlatform, GameProfile};
use crate::net::{DisconnectReason, PlayerConfig}; use crate::net::{DisconnectReason, PlayerConfig};
use crate::plugin::player::exp_change::PlayerExpChangeEvent; 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_change_world::PlayerChangeWorldEvent;
use crate::plugin::player::player_gamemode_change::PlayerGamemodeChangeEvent; use crate::plugin::player::player_gamemode_change::PlayerGamemodeChangeEvent;
use crate::plugin::player::player_permission_check::PlayerPermissionCheckEvent; use crate::plugin::player::player_permission_check::PlayerPermissionCheckEvent;
@ -463,9 +466,14 @@ pub struct Player {
pub tab_list_order: AtomicI32, pub tab_list_order: AtomicI32,
pub tab_list_latency: AtomicI32, pub tab_list_latency: AtomicI32,
pub tab_list_listed: AtomicBool, pub tab_list_listed: AtomicBool,
pub this: Weak<Self>,
} }
impl Player { impl Player {
pub fn this(&self) -> Arc<Self> {
self.this.upgrade().expect("Player should have a reference")
}
#[expect(clippy::too_many_lines)] #[expect(clippy::too_many_lines)]
pub async fn new( pub async fn new(
client: ClientPlatform, client: ClientPlatform,
@ -594,7 +602,8 @@ impl Player {
display_name: Mutex::new(None), display_name: Mutex::new(None),
tab_list_order: AtomicI32::new(0), tab_list_order: AtomicI32::new(0),
tab_list_latency: 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) { pub async fn on_handled_screen_closed(&self) {
self.current_screen_handler let window_type = {
.lock() let handler_lock = self.current_screen_handler.lock().await;
.await let mut handler = handler_lock.lock().await;
.lock() let wt = handler.window_type();
.await handler.on_closed(self).await;
.on_closed(self) wt
};
if let Some(server) = self.living_entity.entity.world.load().server.upgrade() {
server
.plugin_manager
.fire(InventoryCloseEvent::new(&self.this(), window_type))
.await; .await;
}
let player_screen_handler: Arc<Mutex<dyn ScreenHandler>> = let player_screen_handler: Arc<Mutex<dyn ScreenHandler>> =
self.player_screen_handler.clone(); self.player_screen_handler.clone();
@ -3009,14 +3025,23 @@ impl Player {
self.open_container_pos.store(None); 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(); self.update_last_action_time();
let screen_handler = self.current_screen_handler.lock().await; let screen_handler_arc = self.current_screen_handler.lock().await.clone();
let mut screen_handler = screen_handler.lock().await; let mut screen_handler = screen_handler_arc.lock().await;
let behaviour = screen_handler.get_behaviour();
// behaviour is dropped here let (sync_id, container_slots, allow_grab_items, allow_put_items) = {
if i32::from(behaviour.sync_id) != packet.sync_id.0 { 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; return;
} }
@ -3046,7 +3071,146 @@ impl Player {
return; 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.disable_sync();
screen_handler screen_handler
@ -3069,7 +3233,6 @@ impl Player {
screen_handler.update_to_client().await; screen_handler.update_to_client().await;
} else { } else {
screen_handler.send_content_updates().await; screen_handler.send_content_updates().await;
drop(screen_handler);
} }
} }

View File

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

View File

@ -52,10 +52,17 @@ impl ItemBehaviour for EnderPearlItem {
// Consume item // Consume item
let held_item = player.inventory.held_item(); let held_item = player.inventory.held_item();
let consumed = {
let mut main_hand = held_item.lock().await; let mut main_hand = held_item.lock().await;
if !main_hand.is_empty() && main_hand.item.id == Item::ENDER_PEARL.id { if !main_hand.is_empty() && main_hand.item.id == Item::ENDER_PEARL.id {
main_hand.decrement_unless_creative(player.gamemode.load(), 1); main_hand.decrement_unless_creative(player.gamemode.load(), 1);
true
} else { } else {
false
}
};
if !consumed {
let off_hand_item = player.inventory.off_hand_item().await; let off_hand_item = player.inventory.off_hand_item().await;
let mut off_hand = off_hand_item.lock().await; let mut off_hand = off_hand_item.lock().await;
if !off_hand.is_empty() && off_hand.item.id == Item::ENDER_PEARL.id { 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 crate::item::{ItemBehaviour, ItemMetadata};
use pumpkin_data::entity::EntityType; use pumpkin_data::entity::EntityType;
use pumpkin_data::item::Item; use pumpkin_data::item::Item;
use pumpkin_data::item_stack::ItemStack;
use pumpkin_data::sound::Sound; use pumpkin_data::sound::Sound;
pub struct PotionItem; pub struct PotionItem;
@ -72,17 +73,20 @@ impl ItemBehaviour for SplashPotionItem {
// Copy the held item stack data into the projectile // Copy the held item stack data into the projectile
let main = player.inventory.held_item(); let main = player.inventory.held_item();
let mut used_main = true; let mut used_main = true;
let mut stack = main.lock().await.clone(); let mut stack = {
if stack.is_empty() || stack.item.id != pumpkin_data::item::Item::SPLASH_POTION.id { 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 = player.inventory.off_hand_item().await;
let off_stack = off.lock().await.clone(); let s = off.lock().await.clone();
if !off_stack.is_empty() if !s.is_empty() && s.item.id == pumpkin_data::item::Item::SPLASH_POTION.id {
&& off_stack.item.id == pumpkin_data::item::Item::SPLASH_POTION.id stack = Some(s);
{
stack = off_stack;
used_main = false; used_main = false;
} }
} }
let stack = stack.unwrap_or_else(|| ItemStack::EMPTY.clone());
splash.set_item_stack(stack).await; splash.set_item_stack(stack).await;
let yaw = player.living_entity.entity.yaw.load(); 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 // Copy the held item stack data into the projectile
let main = player.inventory.held_item(); let main = player.inventory.held_item();
let mut used_main = true; let mut used_main = true;
let mut stack = main.lock().await.clone(); let mut stack = {
if stack.is_empty() || stack.item.id != pumpkin_data::item::Item::LINGERING_POTION.id { 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 = player.inventory.off_hand_item().await;
let off_stack = off.lock().await.clone(); let s = off.lock().await.clone();
if !off_stack.is_empty() if !s.is_empty() && s.item.id == pumpkin_data::item::Item::LINGERING_POTION.id {
&& off_stack.item.id == pumpkin_data::item::Item::LINGERING_POTION.id stack = Some(s);
{
stack = off_stack;
used_main = false; used_main = false;
} }
} }
let stack = stack.unwrap_or_else(|| ItemStack::EMPTY.clone());
ling.set_item_stack(stack).await; ling.set_item_stack(stack).await;
let yaw = player.living_entity.entity.yaw.load(); let yaw = player.living_entity.entity.yaw.load();

View File

@ -51,10 +51,17 @@ impl ItemBehaviour for SnowBallItem {
// Consume item // Consume item
let held_item = player.inventory.held_item(); let held_item = player.inventory.held_item();
let consumed = {
let mut main_hand = held_item.lock().await; let mut main_hand = held_item.lock().await;
if !main_hand.is_empty() && main_hand.item.id == Item::SNOWBALL.id { if !main_hand.is_empty() && main_hand.item.id == Item::SNOWBALL.id {
main_hand.decrement_unless_creative(player.gamemode.load(), 1); main_hand.decrement_unless_creative(player.gamemode.load(), 1);
true
} else { } else {
false
}
};
if !consumed {
let off_hand_item = player.inventory.off_hand_item().await; let off_hand_item = player.inventory.off_hand_item().await;
let mut off_hand = off_hand_item.lock().await; let mut off_hand = off_hand_item.lock().await;
if !off_hand.is_empty() && off_hand.item.id == Item::SNOWBALL.id { 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) => { id if id == SClickSlot::to_id(version) => {
player player
.on_slot_click(SClickSlot::read(payload, &version)?) .on_slot_click(SClickSlot::read(payload, &version)?, server)
.await; .await;
} }
id if id == SSetHeldItem::to_id(version) => { id if id == SSetHeldItem::to_id(version) => {

View File

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

View File

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

View File

@ -1,6 +1,7 @@
use std::sync::Arc; use std::sync::Arc;
use pumpkin_data::{Block, entity::EntityType}; use pumpkin_data::{Block, entity::EntityType};
use pumpkin_inventory::screen_handler::ClickType;
use pumpkin_protocol::java::server::play::ActionType; use pumpkin_protocol::java::server::play::ActionType;
use pumpkin_util::{ use pumpkin_util::{
GameMode, Hand, 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( pub(super) fn consume_player(
state: &mut PluginHostState, state: &mut PluginHostState,
player: &Resource<pumpkin::plugin::player::Player>, player: &Resource<pumpkin::plugin::player::Player>,

View File

@ -4,13 +4,15 @@ use crate::plugin::{
wit::v0_1::{ wit::v0_1::{
events::{ events::{
ToFromWasmEvent, consume_player, consume_text_component, consume_world, ToFromWasmEvent, consume_player, consume_text_component, consume_world,
from_wasm_entity_interaction_action, from_wasm_entity_type, from_wasm_game_mode, from_wasm_click_type, from_wasm_entity_interaction_action, from_wasm_entity_type,
from_wasm_hand, from_wasm_position, to_wasm_block_position, 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_click_type, to_wasm_entity_interaction_action, to_wasm_entity_type,
to_wasm_hand, to_wasm_position, to_wasm_game_mode, to_wasm_hand, to_wasm_position,
}, },
player::to_wit_item_stack,
pumpkin::plugin::event::{ pumpkin::plugin::event::{
Event, InteractAction as WasmInteractAction, PlayerChangeWorldEventData, Event, InteractAction as WasmInteractAction, InventoryClickEventData,
InventoryCloseEventData, PlayerChangeWorldEventData,
PlayerChangedMainHandEventData, PlayerChatEventData, PlayerCommandSendEventData, PlayerChangedMainHandEventData, PlayerChatEventData, PlayerCommandSendEventData,
PlayerCustomPayloadEventData, PlayerEggThrowEventData, PlayerExpChangeEventData, PlayerCustomPayloadEventData, PlayerEggThrowEventData, PlayerExpChangeEventData,
PlayerFishEventData, PlayerFishState as WasmPlayerFishState, PlayerFishEventData, PlayerFishState as WasmPlayerFishState,
@ -28,6 +30,8 @@ use crate::plugin::{
egg_throw::PlayerEggThrowEvent, egg_throw::PlayerEggThrowEvent,
exp_change::PlayerExpChangeEvent, exp_change::PlayerExpChangeEvent,
fish::{PlayerFishEvent, PlayerFishState}, fish::{PlayerFishEvent, PlayerFishState},
inventory_close::InventoryCloseEvent,
inventory_interact::InventoryClickEvent,
item_held::PlayerItemHeldEvent, item_held::PlayerItemHeldEvent,
player_change_world::PlayerChangeWorldEvent, player_change_world::PlayerChangeWorldEvent,
player_chat::PlayerChatEvent, 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 { impl ToFromWasmEvent for PlayerJoinEvent {
fn to_wasm_event(&self, state: &mut PluginHostState) -> Event { fn to_wasm_event(&self, state: &mut PluginHostState) -> Event {
let player = state let player = state

View File

@ -74,6 +74,8 @@ impl gui::HostGui for PluginHostState {
window_type, window_type,
title, title,
inventory: Arc::new(PluginInventory::new(size)), inventory: Arc::new(PluginInventory::new(size)),
allow_grab_items: true,
allow_put_items: true,
})); }));
self.add_gui(gui) self.add_gui(gui)
@ -177,6 +179,36 @@ impl gui::HostGui for PluginHostState {
Ok(()) 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<()> { async fn drop(&mut self, rep: Resource<Gui>) -> wasmtime::Result<()> {
self.resource_table self.resource_table
.delete::<GuiResource>(Resource::new_own(rep.rep())) .delete::<GuiResource>(Resource::new_own(rep.rep()))

View File

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

View File

@ -430,7 +430,8 @@ impl Server {
} }
// Wrap in Arc after data is loaded // 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! {{ send_cancellable! {{
self; self;

View File

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

View File

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

View File

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