From 7f21c3cfe2d3fda8b573a481e4beceec7b1dc890 Mon Sep 17 00:00:00 2001 From: Alexander Medvedev Date: Sun, 26 Apr 2026 13:49:13 +0200 Subject: [PATCH] feat(plugin-api): add some more events --- Cargo.lock | 1 + pumpkin-inventory/src/gui_builder.rs | 189 ++++++++++++++++ pumpkin-inventory/src/lib.rs | 1 + pumpkin-inventory/src/screen_handler.rs | 30 +++ pumpkin-plugin-wit/v0.1/common.wit | 14 ++ pumpkin-plugin-wit/v0.1/event.wit | 23 +- pumpkin-plugin-wit/v0.1/gui.wit | 14 +- pumpkin/Cargo.toml | 1 + pumpkin/src/entity/ai/control/look_control.rs | 11 +- pumpkin/src/entity/ai/goal/melee_attack.rs | 20 +- pumpkin/src/entity/ai/pathfinder/mod.rs | 15 +- pumpkin/src/entity/living.rs | 30 ++- pumpkin/src/entity/mob/enderman.rs | 17 +- pumpkin/src/entity/mob/mod.rs | 2 +- pumpkin/src/entity/player.rs | 199 +++++++++++++++-- pumpkin/src/item/items/egg.rs | 15 +- pumpkin/src/item/items/ender_pearl.rs | 15 +- pumpkin/src/item/items/potions.rs | 35 +-- pumpkin/src/item/items/snowball.rs | 15 +- pumpkin/src/net/java/mod.rs | 2 +- pumpkin/src/net/java/play.rs | 72 +++--- .../api/events/player/inventory_close.rs | 42 ++++ .../api/events/player/inventory_interact.rs | 86 +++++++ pumpkin/src/plugin/api/events/player/mod.rs | 2 + pumpkin/src/plugin/api/gui.rs | 26 ++- .../wasm/wasm_host/wit/v0_1/events/mod.rs | 31 +++ .../wasm/wasm_host/wit/v0_1/events/player.rs | 74 ++++++- .../loader/wasm/wasm_host/wit/v0_1/gui.rs | 32 +++ .../loader/wasm/wasm_host/wit/v0_1/player.rs | 2 + pumpkin/src/server/mod.rs | 3 +- pumpkin/src/world/dragon_fight.rs | 65 ++++-- pumpkin/src/world/mod.rs | 107 +++++---- pumpkin/src/world/natural_spawner.rs | 209 ++++++++++-------- 33 files changed, 1132 insertions(+), 268 deletions(-) create mode 100644 pumpkin-inventory/src/gui_builder.rs create mode 100644 pumpkin/src/plugin/api/events/player/inventory_close.rs create mode 100644 pumpkin/src/plugin/api/events/player/inventory_interact.rs diff --git a/Cargo.lock b/Cargo.lock index 2de003d04..a052162d3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2795,6 +2795,7 @@ dependencies = [ "bytes", "console-subscriber", "crossbeam", + "dashmap", "flate2", "futures", "hmac 0.13.0", diff --git a/pumpkin-inventory/src/gui_builder.rs b/pumpkin-inventory/src/gui_builder.rs new file mode 100644 index 000000000..0589c3a65 --- /dev/null +++ b/pumpkin-inventory/src/gui_builder.rs @@ -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, + 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) -> 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, + ) -> 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 = player_inventory.clone(); + handler.add_player_slots(&player_inventory_trait); + + handler + } +} + +pub struct GUIScreenHandler { + pub inventory: Arc, + 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 + }) + } +} diff --git a/pumpkin-inventory/src/lib.rs b/pumpkin-inventory/src/lib.rs index a0ebf7971..4e43e9179 100644 --- a/pumpkin-inventory/src/lib.rs +++ b/pumpkin-inventory/src/lib.rs @@ -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; diff --git a/pumpkin-inventory/src/screen_handler.rs b/pumpkin-inventory/src/screen_handler.rs index fac04377c..6effe64c7 100644 --- a/pumpkin-inventory/src/screen_handler.rs +++ b/pumpkin-inventory/src/screen_handler.rs @@ -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, /// Slots selected during a drag operation (for multi-slot distribution). pub drag_slots: Vec, + /// 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, } } diff --git a/pumpkin-plugin-wit/v0.1/common.wit b/pumpkin-plugin-wit/v0.1/common.wit index 24d796edf..5c511132d 100644 --- a/pumpkin-plugin-wit/v0.1/common.wit +++ b/pumpkin-plugin-wit/v0.1/common.wit @@ -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, diff --git a/pumpkin-plugin-wit/v0.1/event.wit b/pumpkin-plugin-wit/v0.1/event.wit index cb826895c..420166377 100644 --- a/pumpkin-plugin-wit/v0.1/event.wit +++ b/pumpkin-plugin-wit/v0.1/event.wit @@ -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, + click-type: click-type, + slot: s16, + raw-slot: s16, + clicked-item: option, + cursor: option, + hotbar-button: s32, + cancelled: bool + } + + record inventory-close-event-data { + player: player, + window-type: option + } + 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), diff --git a/pumpkin-plugin-wit/v0.1/gui.wit b/pumpkin-plugin-wit/v0.1/gui.wit index 6dee603b5..58eb021eb 100644 --- a/pumpkin-plugin-wit/v0.1/gui.wit +++ b/pumpkin-plugin-wit/v0.1/gui.wit @@ -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; } } diff --git a/pumpkin/Cargo.toml b/pumpkin/Cargo.toml index 6d1045efa..d1176f871 100644 --- a/pumpkin/Cargo.toml +++ b/pumpkin/Cargo.toml @@ -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 = [ diff --git a/pumpkin/src/entity/ai/control/look_control.rs b/pumpkin/src/entity/ai/control/look_control.rs index 7d97c84b9..e61269bdf 100644 --- a/pumpkin/src/entity/ai/control/look_control.rs +++ b/pumpkin/src/entity/ai/control/look_control.rs @@ -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( diff --git a/pumpkin/src/entity/ai/goal/melee_attack.rs b/pumpkin/src/entity/ai/goal/melee_attack.rs index 25c00f0a9..43078a85e 100644 --- a/pumpkin/src/entity/ai/goal/melee_attack.rs +++ b/pumpkin/src/entity/ai/goal/melee_attack.rs @@ -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); diff --git a/pumpkin/src/entity/ai/pathfinder/mod.rs b/pumpkin/src/entity/ai/pathfinder/mod.rs index 6156ecf40..7a3757832 100644 --- a/pumpkin/src/entity/ai/pathfinder/mod.rs +++ b/pumpkin/src/entity/ai/pathfinder/mod.rs @@ -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, + /// 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) } } diff --git a/pumpkin/src/entity/living.rs b/pumpkin/src/entity/living.rs index b56cc1433..eacd792b7 100644 --- a/pumpkin/src/entity/living.rs +++ b/pumpkin/src/entity/living.rs @@ -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>> = { + 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>, 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; diff --git a/pumpkin/src/entity/mob/enderman.rs b/pumpkin/src/entity/mob/enderman.rs index 4eca17778..e1b3690f7 100644 --- a/pumpkin/src/entity/mob/enderman.rs +++ b/pumpkin/src/entity/mob/enderman.rs @@ -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 head_item = equipment.get(&EquipmentSlot::HEAD); - let head_stack = head_item.lock().await; - if !head_stack.is_empty() && head_stack.item == &Item::CARVED_PUMPKIN { - return false; + 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.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(); diff --git a/pumpkin/src/entity/mob/mod.rs b/pumpkin/src/entity/mob/mod.rs index ae0d3dace..5a3ac9d8e 100644 --- a/pumpkin/src/entity/mob/mod.rs +++ b/pumpkin/src/entity/mob/mod.rs @@ -355,7 +355,7 @@ impl 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; diff --git a/pumpkin/src/entity/player.rs b/pumpkin/src/entity/player.rs index e9bbbcc93..72a22e574 100644 --- a/pumpkin/src/entity/player.rs +++ b/pumpkin/src/entity/player.rs @@ -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, } impl Player { + pub fn this(&self) -> Arc { + 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) - .await; + 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> = 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); } } diff --git a/pumpkin/src/item/items/egg.rs b/pumpkin/src/item/items/egg.rs index 51a250e09..c538e4712 100644 --- a/pumpkin/src/item/items/egg.rs +++ b/pumpkin/src/item/items/egg.rs @@ -54,10 +54,17 @@ impl ItemBehaviour for EggItem { // Consume item let held_item = player.inventory.held_item(); - 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); - } else { + 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) { diff --git a/pumpkin/src/item/items/ender_pearl.rs b/pumpkin/src/item/items/ender_pearl.rs index b196b2e0e..5cb52f73c 100644 --- a/pumpkin/src/item/items/ender_pearl.rs +++ b/pumpkin/src/item/items/ender_pearl.rs @@ -52,10 +52,17 @@ impl ItemBehaviour for EnderPearlItem { // Consume item let held_item = player.inventory.held_item(); - 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); - } else { + 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 { diff --git a/pumpkin/src/item/items/potions.rs b/pumpkin/src/item/items/potions.rs index 709d1eea9..dc165241e 100644 --- a/pumpkin/src/item/items/potions.rs +++ b/pumpkin/src/item/items/potions.rs @@ -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(); diff --git a/pumpkin/src/item/items/snowball.rs b/pumpkin/src/item/items/snowball.rs index 3ca1acdd1..391053737 100644 --- a/pumpkin/src/item/items/snowball.rs +++ b/pumpkin/src/item/items/snowball.rs @@ -51,10 +51,17 @@ impl ItemBehaviour for SnowBallItem { // Consume item let held_item = player.inventory.held_item(); - 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); - } else { + 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 { diff --git a/pumpkin/src/net/java/mod.rs b/pumpkin/src/net/java/mod.rs index c3f0842a6..69cd964eb 100644 --- a/pumpkin/src/net/java/mod.rs +++ b/pumpkin/src/net/java/mod.rs @@ -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) => { diff --git a/pumpkin/src/net/java/play.rs b/pumpkin/src/net/java/play.rs index 47d4094d8..da9b6ccc8 100644 --- a/pumpkin/src/net/java/play.rs +++ b/pumpkin/src/net/java/play.rs @@ -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 screen_handler_arc = player.current_screen_handler.lock().await.clone(); - let mut handler = screen_handler_arc.lock().await; + let (grid_width, crafting_inv) = { + let screen_handler_arc = player.current_screen_handler.lock().await.clone(); + 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; }; - match handler.window_type() { - Some(WindowType::Furnace | WindowType::BlastFurnace | WindowType::Smoker) => {} - _ => 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) => { + 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, 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); + { + let screen_handler = player.current_screen_handler.lock().await; + let mut screen_handler = screen_handler.lock().await; + screen_handler.sync_state().await; + }; // Restore abilities based on gamemode after respawn - let mut abilities = player.abilities.lock().await; - abilities.set_for_gamemode(player.gamemode.load()); - drop(abilities); + { + let mut abilities = player.abilities.lock().await; + abilities.set_for_gamemode(player.gamemode.load()); + }; 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; diff --git a/pumpkin/src/plugin/api/events/player/inventory_close.rs b/pumpkin/src/plugin/api/events/player/inventory_close.rs new file mode 100644 index 000000000..24a4bbfaf --- /dev/null +++ b/pumpkin/src/plugin/api/events/player/inventory_close.rs @@ -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, + + /// The window type of the inventory that was closed. + pub window_type: Option, +} + +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, window_type: Option) -> Self { + Self { + player: Arc::clone(player), + window_type, + } + } +} + +impl PlayerEvent for InventoryCloseEvent { + fn get_player(&self) -> &Arc { + &self.player + } +} diff --git a/pumpkin/src/plugin/api/events/player/inventory_interact.rs b/pumpkin/src/plugin/api/events/player/inventory_interact.rs new file mode 100644 index 000000000..b4892c6f1 --- /dev/null +++ b/pumpkin/src/plugin/api/events/player/inventory_interact.rs @@ -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, + + /// The window type of the inventory being interacted with. + pub window_type: Option, + + /// 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, + + /// The current `ItemStack` on the cursor. + pub cursor: Option, + + /// 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, + window_type: Option, + click_type: ClickType, + slot: i16, + raw_slot: i16, + clicked_item: Option, + cursor: Option, + 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 { + &self.player + } +} diff --git a/pumpkin/src/plugin/api/events/player/mod.rs b/pumpkin/src/plugin/api/events/player/mod.rs index 44b49793e..480d46f22 100644 --- a/pumpkin/src/plugin/api/events/player/mod.rs +++ b/pumpkin/src/plugin/api/events/player/mod.rs @@ -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; diff --git a/pumpkin/src/plugin/api/gui.rs b/pumpkin/src/plugin/api/gui.rs index fdfa8760f..5e9ad85ee 100644 --- a/pumpkin/src/plugin/api/gui.rs +++ b/pumpkin/src/plugin/api/gui.rs @@ -14,6 +14,8 @@ pub struct PluginGui { pub window_type: WindowType, pub title: TextComponent, pub inventory: Arc, + 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) -> Self { + pub fn new( + sync_id: u8, + window_type: WindowType, + inventory: &Arc, + 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; }) } diff --git a/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/events/mod.rs b/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/events/mod.rs index 2945d58ee..b3b2ab789 100644 --- a/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/events/mod.rs +++ b/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/events/mod.rs @@ -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, diff --git a/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/events/player.rs b/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/events/player.rs index 054c0f1a8..08dc0dc10 100644 --- a/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/events/player.rs +++ b/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/events/player.rs @@ -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 diff --git a/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/gui.rs b/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/gui.rs index 9810497bb..08b132dac 100644 --- a/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/gui.rs +++ b/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/gui.rs @@ -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, + 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) -> wasmtime::Result { + 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, + 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) -> wasmtime::Result { + let gui = self.get_gui_res(&res)?.provider.lock().await; + Ok(gui.allow_put_items) + } + async fn drop(&mut self, rep: Resource) -> wasmtime::Result<()> { self.resource_table .delete::(Resource::new_own(rep.rep())) diff --git a/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/player.rs b/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/player.rs index 1389774d1..4b02f1cc9 100644 --- a/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/player.rs +++ b/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/player.rs @@ -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 diff --git a/pumpkin/src/server/mod.rs b/pumpkin/src/server/mod.rs index bb82c682c..cd64a285f 100644 --- a/pumpkin/src/server/mod.rs +++ b/pumpkin/src/server/mod.rs @@ -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; diff --git a/pumpkin/src/world/dragon_fight.rs b/pumpkin/src/world/dragon_fight.rs index 82031bbba..4880e686c 100644 --- a/pumpkin/src/world/dragon_fight.rs +++ b/pumpkin/src/world/dragon_fight.rs @@ -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) { + pub async fn tick(fight_mutex: &Mutex, world: &Arc) { + 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; } } } diff --git a/pumpkin/src/world/mod.rs b/pumpkin/src/world/mod.rs index 3b940710d..2f6666c6e 100644 --- a/pumpkin/src/world/mod.rs +++ b/pumpkin/src/world/mod.rs @@ -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,44 +849,53 @@ impl World { } async fn tick_environment(&self) { - let mut level_time = self.level_time.lock().await; - let (advance_time, advance_weather) = { - let lock = self.level_info.load(); - ( - lock.game_rules.advance_time, - lock.game_rules.advance_weather, - ) - }; - level_time.tick_time(advance_time, advance_weather); + 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(); + ( + lock.game_rules.advance_time, + lock.game_rules.advance_weather, + ) + }; + level_time.tick_time(advance_time, advance_weather); - // Auto-save logic - if level_time.world_age % 100 == 0 { - self.level.should_unload.store(true, Relaxed); - // If autosave is configured and this tick will trigger an autosave, don't double notify - if self.level.autosave_ticks == 0 { - self.level.level_channel.notify(); - } else { + // Auto-save logic + if level_time.world_age % 100 == 0 { + self.level.should_unload.store(true, Relaxed); + // If autosave is configured and this tick will trigger an autosave, don't double notify + if self.level.autosave_ticks == 0 { + self.level.level_channel.notify(); + } else { + let autosave = self.level.autosave_ticks as i64; + if autosave == 0 || level_time.world_age % autosave != 0 { + self.level.level_channel.notify(); + } + } + } + if self.level.autosave_ticks > 0 { let autosave = self.level.autosave_ticks as i64; - if autosave == 0 || level_time.world_age % autosave != 0 { + if autosave > 0 && level_time.world_age % autosave == 0 { + self.level.should_save.store(true, Relaxed); self.level.level_channel.notify(); } } - } - if self.level.autosave_ticks > 0 { - let autosave = self.level.autosave_ticks as i64; - if autosave > 0 && level_time.world_age % autosave == 0 { - self.level.should_save.store(true, Relaxed); - 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) { 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) - .await; + 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 = self.clone(); + let mut block_entity_tasks = JoinSet::new(); for block_entity in tick_data.block_entities { - block_entity.tick(&world).await; + 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, bounding_box: BoundingBox) -> Vec<&Fluid> { @@ -1349,12 +1377,16 @@ impl World { chunk_pos: Vector2, chunk: &Arc, spawn_list: &Vec<&'static MobCategory>, - spawn_state: &mut SpawnState, + spawn_state: &Arc, ) { // this.level.tickThunder(chunk); //TODO check in simulation distance - let weather = self.weather.lock().await; - if weather.raining && weather.thundering && rng().random_range(0..100_000) == 0 { + let (is_raining, is_thundering) = { + let weather = self.weather.lock().await; + (weather.raining, weather.thundering) + }; + + if is_raining && is_thundering && rng().random_range(0..100_000) == 0 { let rand_value = rng().random::() >> 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; diff --git a/pumpkin/src/world/natural_spawner.rs b/pumpkin/src/world/natural_spawner.rs index de56f47e9..71e5af385 100644 --- a/pumpkin/src/world/natural_spawner.rs +++ b/pumpkin/src/world/natural_spawner.rs @@ -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, - players_near_chunk: HashMap, Vec>, + player_mob_counts: DashMap, + players_near_chunk: DashMap, Vec>, +} + +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", &"") - .field("player_mob_counts", &self.player_mob_counts) - .field("players_near_chunk", &self.players_near_chunk) .finish() } } @@ -67,54 +87,48 @@ impl LocalMobCapCalculator { dx * dx + dy * dy } - fn get_players_near<'b>( - players_near_chunk: &'b mut HashMap, Vec>, - world: &Arc, - chunk_pos: Vector2, - ) -> &'b Vec { - match players_near_chunk.entry(chunk_pos) { - Entry::Occupied(value) => value.into_mut(), - Entry::Vacant(entry) => { - let mut players = Vec::new(); - for player in world.players.load().iter() { - if player.gamemode.load() == GameMode::Spectator { - continue; - } - if Self::calc_distance(chunk_pos, &player.position()) < 16384. { - players.push(player.entity_id()); - } - } - entry.insert(players) + fn get_players_near(&self, world: &Arc, chunk_pos: Vector2) -> Vec { + 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 { + continue; + } + if Self::calc_distance(chunk_pos, &player.position()) < 16384. { + players.push(player.entity_id()); } } + self.players_near_chunk.insert(chunk_pos, players.clone()); + players } + pub fn add_mob( - &mut self, + &self, chunk_pos: Vector2, world: &Arc, 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, chunk_pos: Vector2, ) -> 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); +struct PotentialCalculator(std::sync::Mutex>); 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>, } 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>>, world: &Arc, ) -> 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, category: &'static MobCategory, chunk_pos: Vector2, @@ -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, ) -> 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; - self.spawn_potential - .get_potential_energy_change(pos, cost.charge) - <= cost.energy_budget - } else { - self.last_charge = 0.; - true - } + 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 + }, + ) } - async fn after_spawn( - &mut self, + pub async fn after_spawn( + &self, entity_type: &'static EntityType, pos: &BlockPos, world: &Arc, ) { - 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, chunk_pos: Vector2, chunk: &Arc, - 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, pos: BlockPos, chunk_pos: &Vector2, - spawn_state: &mut SpawnState, + spawn_state: &SpawnState, ) { // TODO StructureManager structureManager = level.structureManager(); // TODO blockState.isRedstoneConductor(chunk, pos) is true then return