feat(plugin-api): add some more events
This commit is contained in:
parent
ee11ba746f
commit
7f21c3cfe2
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -2795,6 +2795,7 @@ dependencies = [
|
||||
"bytes",
|
||||
"console-subscriber",
|
||||
"crossbeam",
|
||||
"dashmap",
|
||||
"flate2",
|
||||
"futures",
|
||||
"hmac 0.13.0",
|
||||
|
||||
189
pumpkin-inventory/src/gui_builder.rs
Normal file
189
pumpkin-inventory/src/gui_builder.rs
Normal 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
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -772,6 +772,13 @@ pub trait ScreenHandler: Send + Sync {
|
||||
})
|
||||
}
|
||||
|
||||
/// Cancels any client-side changes and resynchronizes the state.
|
||||
fn cancel<'a>(&'a mut self, _player: &'a dyn InventoryPlayer) -> ScreenHandlerFuture<'a, ()> {
|
||||
Box::pin(async move {
|
||||
self.sync_state().await;
|
||||
})
|
||||
}
|
||||
|
||||
/// Public entry point for slot click handling.
|
||||
fn on_slot_click<'a>(
|
||||
&'a mut self,
|
||||
@ -1234,6 +1241,26 @@ pub struct ScreenHandlerBehaviour {
|
||||
pub window_type: Option<WindowType>,
|
||||
/// Slots selected during a drag operation (for multi-slot distribution).
|
||||
pub drag_slots: Vec<u32>,
|
||||
/// Whether players can grab items out of the inventory.
|
||||
pub allow_grab_items: bool,
|
||||
/// Whether players can put items into the inventory from their own.
|
||||
pub allow_put_items: bool,
|
||||
/// Number of slots that belong to the container (not the player inventory).
|
||||
pub container_slots: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum ClickType {
|
||||
Left,
|
||||
Right,
|
||||
ShiftLeft,
|
||||
ShiftRight,
|
||||
Middle,
|
||||
Drop,
|
||||
ControlDrop,
|
||||
DoubleClick,
|
||||
NumberKey(u8),
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl ScreenHandlerBehaviour {
|
||||
@ -1254,6 +1281,9 @@ impl ScreenHandlerBehaviour {
|
||||
tracked_property_values: Vec::new(),
|
||||
window_type,
|
||||
drag_slots: Vec::new(),
|
||||
allow_grab_items: true,
|
||||
allow_put_items: true,
|
||||
container_slots: 0,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
interface event {
|
||||
use common.{block-position, game-mode, hand, item-stack, position};
|
||||
use common.{block-position, game-mode, hand, item-stack, position, click-type};
|
||||
use player.{player};
|
||||
use text.{text-component};
|
||||
use %world.{%world};
|
||||
@ -179,6 +179,23 @@ interface event {
|
||||
cancelled: bool
|
||||
}
|
||||
|
||||
record inventory-click-event-data {
|
||||
player: player,
|
||||
window-type: option<string>,
|
||||
click-type: click-type,
|
||||
slot: s16,
|
||||
raw-slot: s16,
|
||||
clicked-item: option<item-stack>,
|
||||
cursor: option<item-stack>,
|
||||
hotbar-button: s32,
|
||||
cancelled: bool
|
||||
}
|
||||
|
||||
record inventory-close-event-data {
|
||||
player: player,
|
||||
window-type: option<string>
|
||||
}
|
||||
|
||||
record block-redstone-event-data {
|
||||
target-world: %world,
|
||||
state-id: u16,
|
||||
@ -272,6 +289,8 @@ interface event {
|
||||
player-toggle-sneak-event,
|
||||
player-toggle-flight-event,
|
||||
player-toggle-sprint-event,
|
||||
inventory-click-event,
|
||||
inventory-close-event,
|
||||
block-redstone-event,
|
||||
block-break-event,
|
||||
block-burn-event,
|
||||
@ -305,6 +324,8 @@ interface event {
|
||||
player-toggle-sneak-event(player-toggle-sneak-event-data),
|
||||
player-toggle-flight-event(player-toggle-flight-event-data),
|
||||
player-toggle-sprint-event(player-toggle-sprint-event-data),
|
||||
inventory-click-event(inventory-click-event-data),
|
||||
inventory-close-event(inventory-close-event-data),
|
||||
block-redstone-event(block-redstone-event-data),
|
||||
block-break-event(block-break-event-data),
|
||||
block-burn-event(block-burn-event-data),
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 = [
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -13,7 +13,7 @@ use crate::entity::ai::pathfinder::walk_node_evaluator::WalkNodeEvaluator;
|
||||
use pumpkin_data::attributes::Attributes;
|
||||
use pumpkin_util::math::wrap_degrees;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
pub mod binary_heap;
|
||||
pub mod node;
|
||||
@ -61,6 +61,9 @@ pub struct Navigator {
|
||||
// Reusable allocations to avoid per-pathfind heap allocations
|
||||
open_set: BinaryHeap,
|
||||
neighbors_buf: Vec<Node>,
|
||||
/// Thread-safe status check to avoid deadlocks when components (like `LookControl`) need to
|
||||
/// check navigation status.
|
||||
pub is_idle: AtomicBool,
|
||||
}
|
||||
|
||||
impl Default for Navigator {
|
||||
@ -79,6 +82,7 @@ impl Default for Navigator {
|
||||
repath_cooldown: 0,
|
||||
open_set: BinaryHeap::new(),
|
||||
neighbors_buf: Vec::new(),
|
||||
is_idle: AtomicBool::new(true),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -95,6 +99,7 @@ const MAX_YAW_TURN_PER_TICK: f32 = 90.0;
|
||||
|
||||
impl Navigator {
|
||||
pub fn set_progress(&mut self, goal: NavigatorGoal) {
|
||||
self.is_idle.store(false, Ordering::Relaxed);
|
||||
self.current_goal = Some(goal);
|
||||
self.current_path = None;
|
||||
}
|
||||
@ -106,6 +111,7 @@ impl Navigator {
|
||||
}
|
||||
|
||||
pub fn stop(&mut self) {
|
||||
self.is_idle.store(true, Ordering::Relaxed);
|
||||
self.current_goal = None;
|
||||
self.current_path = None;
|
||||
self.ticks_on_current_node = 0;
|
||||
@ -300,11 +306,13 @@ impl Navigator {
|
||||
pub async fn tick(&mut self, entity: &LivingEntity) {
|
||||
let Some(goal) = self.current_goal.take() else {
|
||||
// Idle: stop the mob
|
||||
self.is_idle.store(true, Ordering::Relaxed);
|
||||
entity.movement_input.store(Vector3::new(0.0, 0.0, 0.0));
|
||||
return;
|
||||
};
|
||||
|
||||
if goal.current_progress == goal.destination {
|
||||
self.is_idle.store(true, Ordering::Relaxed);
|
||||
self.current_path = None;
|
||||
entity.movement_input.store(Vector3::new(0.0, 0.0, 0.0));
|
||||
return;
|
||||
@ -437,6 +445,7 @@ impl Navigator {
|
||||
.store(false, std::sync::atomic::Ordering::SeqCst);
|
||||
}
|
||||
} else {
|
||||
self.is_idle.store(true, Ordering::Relaxed);
|
||||
self.current_path = None;
|
||||
entity.movement_input.store(Vector3::new(0.0, 0.0, 0.0));
|
||||
}
|
||||
@ -446,7 +455,7 @@ impl Navigator {
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn is_idle(&self) -> bool {
|
||||
self.current_goal.is_none()
|
||||
pub fn is_idle(&self) -> bool {
|
||||
self.is_idle.load(Ordering::Relaxed)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1304,10 +1304,16 @@ impl LivingEntity {
|
||||
|
||||
let block_pos = self.entity.block_pos.load();
|
||||
|
||||
for slot in self.equipment_slots.values() {
|
||||
let armor_slots: Vec<Arc<Mutex<ItemStack>>> = {
|
||||
let equipment_lock = self.entity_equipment.lock().await;
|
||||
self.equipment_slots
|
||||
.values()
|
||||
.map(|slot| equipment_lock.get(slot))
|
||||
.collect()
|
||||
};
|
||||
|
||||
for equipment in armor_slots {
|
||||
let item = {
|
||||
let lock = self.entity_equipment.lock().await;
|
||||
let equipment = lock.get(slot);
|
||||
let mut item_lock = equipment.lock().await;
|
||||
mem::replace(&mut *item_lock, ItemStack::EMPTY.clone())
|
||||
};
|
||||
@ -1532,12 +1538,16 @@ impl LivingEntity {
|
||||
// TODO: Falling anvil/stalactite should only damage the helmet slot.
|
||||
// TODO: Implement DAMAGE_RESISTANT component checks (e.g. netherite vs fire).
|
||||
|
||||
for (slot_index, slot) in self.equipment_slots.iter() {
|
||||
if !slot.is_armor_slot() {
|
||||
continue;
|
||||
}
|
||||
let armor_slots: Vec<(usize, Arc<Mutex<ItemStack>>, EquipmentSlot)> = {
|
||||
let equipment_lock = self.entity_equipment.lock().await;
|
||||
self.equipment_slots
|
||||
.iter()
|
||||
.filter(|(_, slot)| slot.is_armor_slot())
|
||||
.map(|(index, slot)| (*index, equipment_lock.get(slot), slot.clone()))
|
||||
.collect()
|
||||
};
|
||||
|
||||
let equipment = self.entity_equipment.lock().await.get(slot);
|
||||
for (slot_index, equipment, slot) in armor_slots {
|
||||
let (slot_result, updated_stack_opt) = {
|
||||
let mut stack = equipment.lock().await;
|
||||
if stack.is_empty() {
|
||||
@ -1568,14 +1578,14 @@ impl LivingEntity {
|
||||
if slot_result == pumpkin_data::item_stack::DamageResult::Broken {
|
||||
let world = self.entity.world.load();
|
||||
world
|
||||
.send_entity_status(&self.entity, super::equipment_break_status(slot))
|
||||
.send_entity_status(&self.entity, super::equipment_break_status(&slot))
|
||||
.await;
|
||||
}
|
||||
equipment_updates.push((slot.clone(), updated_stack.clone()));
|
||||
if let Some(player) = caller.get_player() {
|
||||
player
|
||||
.enqueue_slot_set_packet(&CSetPlayerInventory::new(
|
||||
(*slot_index as i32).into(),
|
||||
(slot_index as i32).into(),
|
||||
&ItemStackSerializer::from(updated_stack),
|
||||
))
|
||||
.await;
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -355,7 +355,7 @@ impl<T: Mob + Send + 'static> EntityBase for T {
|
||||
drop(navigator);
|
||||
|
||||
let mut look_control = mob_entity.look_control.lock().await;
|
||||
look_control.tick(self).await;
|
||||
look_control.tick(self);
|
||||
drop(look_control);
|
||||
|
||||
mob_entity.living_entity.tick(caller, server).await;
|
||||
|
||||
@ -44,7 +44,8 @@ use pumpkin_inventory::player::{
|
||||
player_inventory::PlayerInventory, player_screen_handler::PlayerScreenHandler,
|
||||
};
|
||||
use pumpkin_inventory::screen_handler::{
|
||||
InventoryPlayer, PlayerFuture, ScreenHandler, ScreenHandlerFactory, ScreenHandlerListener,
|
||||
ClickType, InventoryPlayer, PlayerFuture, ScreenHandler, ScreenHandlerFactory,
|
||||
ScreenHandlerListener,
|
||||
};
|
||||
use pumpkin_inventory::sync_handler::SyncHandler;
|
||||
use pumpkin_macros::send_cancellable;
|
||||
@ -63,7 +64,7 @@ use pumpkin_protocol::java::client::play::{
|
||||
CTitleAnimation, CTitleText, CUnloadChunk, CUpdateMobEffect, CUpdateTime, GameEvent, Metadata,
|
||||
PlayerAction, PlayerInfoFlags, PreviousMessage,
|
||||
};
|
||||
use pumpkin_protocol::java::server::play::SClickSlot;
|
||||
use pumpkin_protocol::java::server::play::{SClickSlot, SlotActionType};
|
||||
use pumpkin_util::math::{
|
||||
boundingbox::BoundingBox, experience, position::BlockPos, vector2::Vector2, vector3::Vector3,
|
||||
};
|
||||
@ -87,6 +88,8 @@ use crate::entity::{EntityBaseFuture, NbtFuture, TeleportFuture};
|
||||
use crate::net::{ClientPlatform, GameProfile};
|
||||
use crate::net::{DisconnectReason, PlayerConfig};
|
||||
use crate::plugin::player::exp_change::PlayerExpChangeEvent;
|
||||
use crate::plugin::player::inventory_close::InventoryCloseEvent;
|
||||
use crate::plugin::player::inventory_interact::InventoryClickEvent;
|
||||
use crate::plugin::player::player_change_world::PlayerChangeWorldEvent;
|
||||
use crate::plugin::player::player_gamemode_change::PlayerGamemodeChangeEvent;
|
||||
use crate::plugin::player::player_permission_check::PlayerPermissionCheckEvent;
|
||||
@ -463,9 +466,14 @@ pub struct Player {
|
||||
pub tab_list_order: AtomicI32,
|
||||
pub tab_list_latency: AtomicI32,
|
||||
pub tab_list_listed: AtomicBool,
|
||||
pub this: Weak<Self>,
|
||||
}
|
||||
|
||||
impl Player {
|
||||
pub fn this(&self) -> Arc<Self> {
|
||||
self.this.upgrade().expect("Player should have a reference")
|
||||
}
|
||||
|
||||
#[expect(clippy::too_many_lines)]
|
||||
pub async fn new(
|
||||
client: ClientPlatform,
|
||||
@ -594,7 +602,8 @@ impl Player {
|
||||
display_name: Mutex::new(None),
|
||||
tab_list_order: AtomicI32::new(0),
|
||||
tab_list_latency: AtomicI32::new(0),
|
||||
tab_list_listed: AtomicBool::new(true),
|
||||
tab_list_listed: AtomicBool::new(false),
|
||||
this: Weak::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -2888,13 +2897,20 @@ impl Player {
|
||||
}
|
||||
|
||||
pub async fn on_handled_screen_closed(&self) {
|
||||
self.current_screen_handler
|
||||
.lock()
|
||||
.await
|
||||
.lock()
|
||||
.await
|
||||
.on_closed(self)
|
||||
.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<Mutex<dyn ScreenHandler>> =
|
||||
self.player_screen_handler.clone();
|
||||
@ -3009,14 +3025,23 @@ impl Player {
|
||||
self.open_container_pos.store(None);
|
||||
}
|
||||
|
||||
pub async fn on_slot_click(&self, packet: SClickSlot) {
|
||||
#[allow(clippy::too_many_lines)]
|
||||
pub async fn on_slot_click(&self, packet: SClickSlot, server: &Server) {
|
||||
self.update_last_action_time();
|
||||
let screen_handler = self.current_screen_handler.lock().await;
|
||||
let mut screen_handler = screen_handler.lock().await;
|
||||
let behaviour = screen_handler.get_behaviour();
|
||||
let screen_handler_arc = self.current_screen_handler.lock().await.clone();
|
||||
let mut screen_handler = screen_handler_arc.lock().await;
|
||||
|
||||
// behaviour is dropped here
|
||||
if i32::from(behaviour.sync_id) != packet.sync_id.0 {
|
||||
let (sync_id, container_slots, allow_grab_items, allow_put_items) = {
|
||||
let b = screen_handler.get_behaviour();
|
||||
(
|
||||
b.sync_id,
|
||||
b.container_slots,
|
||||
b.allow_grab_items,
|
||||
b.allow_put_items,
|
||||
)
|
||||
};
|
||||
|
||||
if i32::from(sync_id) != packet.sync_id.0 {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -3046,7 +3071,146 @@ impl Player {
|
||||
return;
|
||||
}
|
||||
|
||||
let not_in_sync = packet.revision.0 != (behaviour.revision.load(Ordering::Relaxed) as i32);
|
||||
// Fire InventoryClickEvent
|
||||
let clicked_item = if slot >= 0 {
|
||||
let slot_obj = &screen_handler.get_behaviour().slots[slot as usize];
|
||||
Some(slot_obj.get_cloned_stack().await)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let cursor_item = Some(
|
||||
screen_handler
|
||||
.get_behaviour()
|
||||
.cursor_stack
|
||||
.lock()
|
||||
.await
|
||||
.clone(),
|
||||
);
|
||||
let raw_slot = slot; // For now raw_slot == slot, as we don't have separate view/inventory indexing yet
|
||||
let hotbar_button = if matches!(packet.mode, SlotActionType::Swap) {
|
||||
packet.button
|
||||
} else {
|
||||
-1
|
||||
};
|
||||
|
||||
let click_type = match packet.mode {
|
||||
SlotActionType::Pickup => {
|
||||
if packet.button == 0 {
|
||||
ClickType::Left
|
||||
} else {
|
||||
ClickType::Right
|
||||
}
|
||||
}
|
||||
SlotActionType::QuickMove => {
|
||||
if packet.button == 0 {
|
||||
ClickType::ShiftLeft
|
||||
} else {
|
||||
ClickType::ShiftRight
|
||||
}
|
||||
}
|
||||
SlotActionType::Swap => ClickType::NumberKey(packet.button as u8),
|
||||
SlotActionType::Clone => ClickType::Middle,
|
||||
SlotActionType::Throw => {
|
||||
if packet.button == 0 {
|
||||
ClickType::Drop
|
||||
} else {
|
||||
ClickType::ControlDrop
|
||||
}
|
||||
}
|
||||
SlotActionType::QuickCraft => {
|
||||
if [0, 4, 8].contains(&packet.button) {
|
||||
ClickType::Left
|
||||
} else if [1, 5, 9].contains(&packet.button) {
|
||||
ClickType::Right
|
||||
} else {
|
||||
ClickType::Middle
|
||||
}
|
||||
}
|
||||
SlotActionType::PickupAll => ClickType::DoubleClick,
|
||||
};
|
||||
|
||||
send_cancellable! {{
|
||||
server;
|
||||
InventoryClickEvent::new(
|
||||
&self.this(),
|
||||
screen_handler.window_type(),
|
||||
click_type,
|
||||
slot,
|
||||
raw_slot,
|
||||
clicked_item,
|
||||
cursor_item,
|
||||
i32::from(hotbar_button),
|
||||
);
|
||||
'after: {}
|
||||
'cancelled: {
|
||||
screen_handler.cancel(self).await;
|
||||
return;
|
||||
}
|
||||
}}
|
||||
|
||||
// Enforce flags
|
||||
let is_container_slot = slot >= 0 && i32::from(slot) < container_slots as i32;
|
||||
|
||||
match packet.mode {
|
||||
SlotActionType::Pickup => {
|
||||
let cursor_stack = screen_handler.get_behaviour().cursor_stack.lock().await;
|
||||
if is_container_slot {
|
||||
if !cursor_stack.is_empty() && !allow_put_items {
|
||||
drop(cursor_stack);
|
||||
screen_handler.cancel(self).await;
|
||||
return;
|
||||
}
|
||||
if cursor_stack.is_empty() && !allow_grab_items {
|
||||
drop(cursor_stack);
|
||||
screen_handler.cancel(self).await;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
SlotActionType::QuickMove => {
|
||||
if is_container_slot && !allow_grab_items {
|
||||
screen_handler.cancel(self).await;
|
||||
return;
|
||||
}
|
||||
if !is_container_slot && !allow_put_items {
|
||||
screen_handler.cancel(self).await;
|
||||
return;
|
||||
}
|
||||
}
|
||||
SlotActionType::Swap => {
|
||||
if is_container_slot && (!allow_grab_items || !allow_put_items) {
|
||||
screen_handler.cancel(self).await;
|
||||
return;
|
||||
}
|
||||
}
|
||||
SlotActionType::Throw => {
|
||||
if is_container_slot && !allow_grab_items {
|
||||
screen_handler.cancel(self).await;
|
||||
return;
|
||||
}
|
||||
}
|
||||
SlotActionType::QuickCraft => {
|
||||
if !allow_put_items {
|
||||
// Dragging items into slots
|
||||
screen_handler.cancel(self).await;
|
||||
return;
|
||||
}
|
||||
}
|
||||
SlotActionType::PickupAll => {
|
||||
if !allow_grab_items {
|
||||
screen_handler.cancel(self).await;
|
||||
return;
|
||||
}
|
||||
}
|
||||
SlotActionType::Clone => {}
|
||||
}
|
||||
|
||||
let not_in_sync = packet.revision.0
|
||||
!= (screen_handler
|
||||
.get_behaviour()
|
||||
.revision
|
||||
.load(Ordering::Relaxed) as i32);
|
||||
|
||||
screen_handler.disable_sync();
|
||||
screen_handler
|
||||
@ -3069,7 +3233,6 @@ impl Player {
|
||||
screen_handler.update_to_client().await;
|
||||
} else {
|
||||
screen_handler.send_content_updates().await;
|
||||
drop(screen_handler);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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<Player>, swing_arm: SSwingArm) {
|
||||
player.update_last_action_time();
|
||||
@ -1546,15 +1555,17 @@ impl JavaClient {
|
||||
}
|
||||
player.world().clone().respawn_player(player, false).await;
|
||||
|
||||
let screen_handler = player.current_screen_handler.lock().await;
|
||||
let mut screen_handler = screen_handler.lock().await;
|
||||
screen_handler.sync_state().await;
|
||||
drop(screen_handler);
|
||||
{
|
||||
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;
|
||||
|
||||
42
pumpkin/src/plugin/api/events/player/inventory_close.rs
Normal file
42
pumpkin/src/plugin/api/events/player/inventory_close.rs
Normal 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
|
||||
}
|
||||
}
|
||||
86
pumpkin/src/plugin/api/events/player/inventory_interact.rs
Normal file
86
pumpkin/src/plugin/api/events/player/inventory_interact.rs
Normal 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
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -14,6 +14,8 @@ pub struct PluginGui {
|
||||
pub window_type: WindowType,
|
||||
pub title: TextComponent,
|
||||
pub inventory: Arc<PluginInventory>,
|
||||
pub allow_grab_items: bool,
|
||||
pub allow_put_items: bool,
|
||||
}
|
||||
|
||||
pub struct PluginInventory {
|
||||
@ -81,6 +83,14 @@ impl Inventory for PluginInventory {
|
||||
})
|
||||
}
|
||||
|
||||
fn on_open(&self) -> InventoryFuture<'_, ()> {
|
||||
Box::pin(async move {})
|
||||
}
|
||||
|
||||
fn on_close(&self) -> InventoryFuture<'_, ()> {
|
||||
Box::pin(async move {})
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
@ -93,10 +103,21 @@ pub struct PluginScreenHandler {
|
||||
|
||||
impl PluginScreenHandler {
|
||||
#[must_use]
|
||||
pub fn new(sync_id: u8, window_type: WindowType, inventory: &Arc<PluginInventory>) -> Self {
|
||||
pub fn new(
|
||||
sync_id: u8,
|
||||
window_type: WindowType,
|
||||
inventory: &Arc<PluginInventory>,
|
||||
allow_grab_items: bool,
|
||||
allow_put_items: bool,
|
||||
) -> Self {
|
||||
let mut behaviour = ScreenHandlerBehaviour::new(sync_id, Some(window_type));
|
||||
behaviour.allow_grab_items = allow_grab_items;
|
||||
behaviour.allow_put_items = allow_put_items;
|
||||
behaviour.container_slots = inventory.size();
|
||||
|
||||
let mut handler = Self {
|
||||
inventory: inventory.clone(),
|
||||
behaviour: ScreenHandlerBehaviour::new(sync_id, Some(window_type)),
|
||||
behaviour,
|
||||
};
|
||||
|
||||
for i in 0..inventory.size() {
|
||||
@ -111,6 +132,7 @@ impl ScreenHandler for PluginScreenHandler {
|
||||
fn on_closed<'a>(&'a mut self, player: &'a dyn InventoryPlayer) -> ScreenHandlerFuture<'a, ()> {
|
||||
Box::pin(async move {
|
||||
self.default_on_closed(player).await;
|
||||
self.inventory.on_close().await;
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use pumpkin_data::{Block, entity::EntityType};
|
||||
use pumpkin_inventory::screen_handler::ClickType;
|
||||
use pumpkin_protocol::java::server::play::ActionType;
|
||||
use pumpkin_util::{
|
||||
GameMode, Hand,
|
||||
@ -137,6 +138,36 @@ pub(super) const fn from_wasm_game_mode(game_mode: pumpkin::plugin::common::Game
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) const fn to_wasm_click_type(click_type: ClickType) -> pumpkin::plugin::gui::ClickType {
|
||||
match click_type {
|
||||
ClickType::Left => pumpkin::plugin::gui::ClickType::Left,
|
||||
ClickType::Right => pumpkin::plugin::gui::ClickType::Right,
|
||||
ClickType::ShiftLeft => pumpkin::plugin::gui::ClickType::ShiftLeft,
|
||||
ClickType::ShiftRight => pumpkin::plugin::gui::ClickType::ShiftRight,
|
||||
ClickType::Middle => pumpkin::plugin::gui::ClickType::Middle,
|
||||
ClickType::Drop => pumpkin::plugin::gui::ClickType::Drop,
|
||||
ClickType::ControlDrop => pumpkin::plugin::gui::ClickType::ControlDrop,
|
||||
ClickType::DoubleClick => pumpkin::plugin::gui::ClickType::DoubleClick,
|
||||
ClickType::NumberKey(_) => pumpkin::plugin::gui::ClickType::NumberKey,
|
||||
ClickType::Unknown => pumpkin::plugin::gui::ClickType::Unknown,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) const fn from_wasm_click_type(click_type: pumpkin::plugin::gui::ClickType) -> ClickType {
|
||||
match click_type {
|
||||
pumpkin::plugin::gui::ClickType::Left => ClickType::Left,
|
||||
pumpkin::plugin::gui::ClickType::Right => ClickType::Right,
|
||||
pumpkin::plugin::gui::ClickType::ShiftLeft => ClickType::ShiftLeft,
|
||||
pumpkin::plugin::gui::ClickType::ShiftRight => ClickType::ShiftRight,
|
||||
pumpkin::plugin::gui::ClickType::Middle => ClickType::Middle,
|
||||
pumpkin::plugin::gui::ClickType::Drop => ClickType::Drop,
|
||||
pumpkin::plugin::gui::ClickType::ControlDrop => ClickType::ControlDrop,
|
||||
pumpkin::plugin::gui::ClickType::DoubleClick => ClickType::DoubleClick,
|
||||
pumpkin::plugin::gui::ClickType::NumberKey => ClickType::NumberKey(0), // Default to 0
|
||||
pumpkin::plugin::gui::ClickType::Unknown => ClickType::Unknown,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn consume_player(
|
||||
state: &mut PluginHostState,
|
||||
player: &Resource<pumpkin::plugin::player::Player>,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -74,6 +74,8 @@ impl gui::HostGui for PluginHostState {
|
||||
window_type,
|
||||
title,
|
||||
inventory: Arc::new(PluginInventory::new(size)),
|
||||
allow_grab_items: true,
|
||||
allow_put_items: true,
|
||||
}));
|
||||
|
||||
self.add_gui(gui)
|
||||
@ -177,6 +179,36 @@ impl gui::HostGui for PluginHostState {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn set_allow_grab_items(
|
||||
&mut self,
|
||||
res: Resource<Gui>,
|
||||
allow: bool,
|
||||
) -> wasmtime::Result<()> {
|
||||
let mut gui = self.get_gui_res(&res)?.provider.lock().await;
|
||||
gui.allow_grab_items = allow;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_allow_grab_items(&mut self, res: Resource<Gui>) -> wasmtime::Result<bool> {
|
||||
let gui = self.get_gui_res(&res)?.provider.lock().await;
|
||||
Ok(gui.allow_grab_items)
|
||||
}
|
||||
|
||||
async fn set_allow_put_items(
|
||||
&mut self,
|
||||
res: Resource<Gui>,
|
||||
allow: bool,
|
||||
) -> wasmtime::Result<()> {
|
||||
let mut gui = self.get_gui_res(&res)?.provider.lock().await;
|
||||
gui.allow_put_items = allow;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_allow_put_items(&mut self, res: Resource<Gui>) -> wasmtime::Result<bool> {
|
||||
let gui = self.get_gui_res(&res)?.provider.lock().await;
|
||||
Ok(gui.allow_put_items)
|
||||
}
|
||||
|
||||
async fn drop(&mut self, rep: Resource<Gui>) -> wasmtime::Result<()> {
|
||||
self.resource_table
|
||||
.delete::<GuiResource>(Resource::new_own(rep.rep()))
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use tokio::sync::Mutex;
|
||||
use tracing::{debug, info};
|
||||
use uuid::Uuid;
|
||||
|
||||
@ -131,45 +132,67 @@ impl DragonFight {
|
||||
|
||||
// ── Main tick ─────────────────────────────────────────────────────────────
|
||||
|
||||
pub async fn tick(&mut self, world: &Arc<World>) {
|
||||
pub async fn tick(fight_mutex: &Mutex<Self>, world: &Arc<World>) {
|
||||
let (
|
||||
ticks_since_last_player_scan,
|
||||
needs_state_scanning,
|
||||
respawn_stage,
|
||||
dragon_killed,
|
||||
dragon_uuid,
|
||||
) = {
|
||||
let mut fight = fight_mutex.lock().await;
|
||||
fight.ticks_since_last_player_scan += 1;
|
||||
(
|
||||
fight.ticks_since_last_player_scan,
|
||||
fight.needs_state_scanning,
|
||||
fight.respawn_stage,
|
||||
fight.dragon_killed,
|
||||
fight.dragon_uuid,
|
||||
)
|
||||
};
|
||||
|
||||
// 1. Update boss-bar recipients every 20 ticks.
|
||||
self.ticks_since_last_player_scan += 1;
|
||||
if self.ticks_since_last_player_scan >= PLAYER_SCAN_INTERVAL {
|
||||
self.update_players(world).await;
|
||||
self.ticks_since_last_player_scan = 0;
|
||||
if ticks_since_last_player_scan >= PLAYER_SCAN_INTERVAL {
|
||||
let mut fight = fight_mutex.lock().await;
|
||||
fight.update_players(world).await;
|
||||
fight.ticks_since_last_player_scan = 0;
|
||||
}
|
||||
|
||||
let is_empty = { fight_mutex.lock().await.bossbar_players.is_empty() };
|
||||
// Nothing to do without nearby players.
|
||||
if self.bossbar_players.is_empty() {
|
||||
if is_empty {
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. One-time state scan on the first populated tick.
|
||||
if self.needs_state_scanning {
|
||||
self.scan_state(world).await;
|
||||
self.needs_state_scanning = false;
|
||||
if needs_state_scanning {
|
||||
let mut fight = fight_mutex.lock().await;
|
||||
fight.scan_state(world).await;
|
||||
fight.needs_state_scanning = false;
|
||||
}
|
||||
|
||||
// 3. Respawn sequence (takes priority over normal dragon-missing logic).
|
||||
if self.respawn_stage.is_some() {
|
||||
self.tick_respawn(world).await;
|
||||
if respawn_stage.is_some() {
|
||||
let mut fight = fight_mutex.lock().await;
|
||||
fight.tick_respawn(world).await;
|
||||
return;
|
||||
}
|
||||
|
||||
// 4. Normal fight ticking.
|
||||
if !self.dragon_killed {
|
||||
self.ticks_since_dragon_seen += 1;
|
||||
if self.dragon_uuid.is_none()
|
||||
|| self.ticks_since_dragon_seen >= MAX_TICKS_BEFORE_DRAGON_RESPAWN
|
||||
if !dragon_killed {
|
||||
let mut fight = fight_mutex.lock().await;
|
||||
fight.ticks_since_dragon_seen += 1;
|
||||
if dragon_uuid.is_none()
|
||||
|| fight.ticks_since_dragon_seen >= MAX_TICKS_BEFORE_DRAGON_RESPAWN
|
||||
{
|
||||
self.find_or_create_dragon(world).await;
|
||||
self.ticks_since_dragon_seen = 0;
|
||||
fight.find_or_create_dragon(world).await;
|
||||
fight.ticks_since_dragon_seen = 0;
|
||||
}
|
||||
|
||||
self.ticks_since_crystals_scanned += 1;
|
||||
if self.ticks_since_crystals_scanned >= CRYSTAL_SCAN_INTERVAL {
|
||||
self.update_crystal_count(world);
|
||||
self.ticks_since_crystals_scanned = 0;
|
||||
fight.ticks_since_crystals_scanned += 1;
|
||||
if fight.ticks_since_crystals_scanned >= CRYSTAL_SCAN_INTERVAL {
|
||||
fight.update_crystal_count(world);
|
||||
fight.ticks_since_crystals_scanned = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<Self>) {
|
||||
let tick_data = self.level.get_tick_data();
|
||||
for scheduled_tick in tick_data.block_ticks {
|
||||
@ -974,7 +986,7 @@ impl World {
|
||||
}
|
||||
}
|
||||
|
||||
let mut spawn_state =
|
||||
let spawn_state =
|
||||
SpawnState::new(spawning_chunks_map.len() as i32, &self.entities, self).await; // TODO store it
|
||||
|
||||
// TODO gamerule this.spawnEnemies || this.spawnFriendlies
|
||||
@ -1002,17 +1014,33 @@ impl World {
|
||||
spawning_chunks_map.into_iter().collect();
|
||||
spawning_chunks.shuffle(&mut rng());
|
||||
|
||||
// TODO i think it can be multithread
|
||||
let mut spawn_tasks = JoinSet::new();
|
||||
let spawn_state = Arc::new(spawn_state);
|
||||
let spawn_list = Arc::new(spawn_list);
|
||||
|
||||
for (pos, chunk) in spawning_chunks {
|
||||
self.tick_spawning_chunk(pos, &chunk, &spawn_list, &mut spawn_state)
|
||||
.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<dyn SimpleWorld> = 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<Self>, bounding_box: BoundingBox) -> Vec<&Fluid> {
|
||||
@ -1349,12 +1377,16 @@ impl World {
|
||||
chunk_pos: Vector2<i32>,
|
||||
chunk: &Arc<ChunkData>,
|
||||
spawn_list: &Vec<&'static MobCategory>,
|
||||
spawn_state: &mut SpawnState,
|
||||
spawn_state: &Arc<SpawnState>,
|
||||
) {
|
||||
// this.level.tickThunder(chunk);
|
||||
//TODO check in simulation distance
|
||||
let 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::<i32>() >> 2;
|
||||
let delta = Vector3::new(rand_value & 15, rand_value >> 16 & 15, rand_value >> 8 & 15);
|
||||
let random_pos = Vector3::new(
|
||||
@ -1395,7 +1427,6 @@ impl World {
|
||||
self.spawn_entity(Arc::new(entity)).await;
|
||||
}
|
||||
}
|
||||
drop(weather);
|
||||
|
||||
if spawn_list.is_empty() {
|
||||
return;
|
||||
|
||||
@ -22,40 +22,60 @@ use pumpkin_world::chunk::io::Dirtiable;
|
||||
use pumpkin_world::chunk::{ChunkData, ChunkHeightmapType};
|
||||
use rand::seq::IndexedRandom;
|
||||
use rand::{RngExt, rng};
|
||||
use std::collections::HashMap;
|
||||
use std::collections::hash_map::Entry;
|
||||
use std::fmt;
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
|
||||
const MAGIC_NUMBER: i32 = 17 * 17;
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub struct MobCounts([i32; 8]);
|
||||
use dashmap::DashMap;
|
||||
use std::sync::atomic::{AtomicI32, Ordering::Relaxed};
|
||||
|
||||
impl MobCounts {
|
||||
#[inline]
|
||||
const fn add(&mut self, category: &'static MobCategory) {
|
||||
self.0[category.id] += 1;
|
||||
}
|
||||
#[inline]
|
||||
const fn can_spawn(&self, category: &'static MobCategory) -> bool {
|
||||
self.0[category.id] < category.max
|
||||
pub struct MobCounts([AtomicI32; 8]);
|
||||
|
||||
impl Default for MobCounts {
|
||||
fn default() -> Self {
|
||||
Self(std::array::from_fn(|_| AtomicI32::new(0)))
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for MobCounts {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_list()
|
||||
.entries(self.0.iter().map(|a| a.load(Relaxed)))
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl MobCounts {
|
||||
#[inline]
|
||||
pub fn add(&self, category: &'static MobCategory) {
|
||||
self.0[category.id].fetch_add(1, Relaxed);
|
||||
}
|
||||
#[inline]
|
||||
pub fn can_spawn(&self, category: &'static MobCategory) -> bool {
|
||||
self.0[category.id].load(Relaxed) < category.max
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct LocalMobCapCalculator {
|
||||
player_mob_counts: HashMap<i32, MobCounts>,
|
||||
players_near_chunk: HashMap<Vector2<i32>, Vec<i32>>,
|
||||
player_mob_counts: DashMap<i32, MobCounts>,
|
||||
players_near_chunk: DashMap<Vector2<i32>, Vec<i32>>,
|
||||
}
|
||||
|
||||
impl Default for LocalMobCapCalculator {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
player_mob_counts: DashMap::new(),
|
||||
players_near_chunk: DashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for LocalMobCapCalculator {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
f.debug_struct("LocalMobCapCalculator")
|
||||
.field("world", &"<skipped>")
|
||||
.field("player_mob_counts", &self.player_mob_counts)
|
||||
.field("players_near_chunk", &self.players_near_chunk)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
@ -67,54 +87,48 @@ impl LocalMobCapCalculator {
|
||||
dx * dx + dy * dy
|
||||
}
|
||||
|
||||
fn get_players_near<'b>(
|
||||
players_near_chunk: &'b mut HashMap<Vector2<i32>, Vec<i32>>,
|
||||
world: &Arc<World>,
|
||||
chunk_pos: Vector2<i32>,
|
||||
) -> &'b Vec<i32> {
|
||||
match players_near_chunk.entry(chunk_pos) {
|
||||
Entry::Occupied(value) => value.into_mut(),
|
||||
Entry::Vacant(entry) => {
|
||||
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<World>, chunk_pos: Vector2<i32>) -> Vec<i32> {
|
||||
if let Some(players) = self.players_near_chunk.get(&chunk_pos) {
|
||||
return players.value().clone();
|
||||
}
|
||||
|
||||
let mut players = Vec::new();
|
||||
for player in world.players.load().iter() {
|
||||
if player.gamemode.load() == GameMode::Spectator {
|
||||
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<i32>,
|
||||
world: &Arc<World>,
|
||||
category: &'static MobCategory,
|
||||
) {
|
||||
let players = Self::get_players_near(&mut self.players_near_chunk, world, chunk_pos);
|
||||
let players = self.get_players_near(world, chunk_pos);
|
||||
for player in players {
|
||||
self.player_mob_counts
|
||||
.entry(*player)
|
||||
.entry(player)
|
||||
.or_default()
|
||||
.add(category);
|
||||
}
|
||||
// debug!("player_mob_counts {:?}", self.player_mob_counts);
|
||||
// debug!("players_near_chunk {:?}", self.players_near_chunk);
|
||||
// debug!("chunk_pos {:?}", chunk_pos);
|
||||
// debug!("players {:?}", players);
|
||||
}
|
||||
|
||||
pub fn can_spawn(
|
||||
&mut self,
|
||||
&self,
|
||||
category: &'static MobCategory,
|
||||
world: &Arc<World>,
|
||||
chunk_pos: Vector2<i32>,
|
||||
) -> bool {
|
||||
let players = Self::get_players_near(&mut self.players_near_chunk, world, chunk_pos);
|
||||
let players = self.get_players_near(world, chunk_pos);
|
||||
for player in players {
|
||||
if let Some(count) = self.player_mob_counts.get(player) {
|
||||
if let Some(count) = self.player_mob_counts.get(&player) {
|
||||
if count.can_spawn(category) {
|
||||
return true;
|
||||
}
|
||||
@ -137,12 +151,15 @@ impl PointCharge {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
struct PotentialCalculator(Vec<PointCharge>);
|
||||
struct PotentialCalculator(std::sync::Mutex<Vec<PointCharge>>);
|
||||
|
||||
impl PotentialCalculator {
|
||||
pub fn add_charge(&mut self, pos: &BlockPos, charge: f64) {
|
||||
pub fn add_charge(&self, pos: &BlockPos, charge: f64) {
|
||||
if charge != 0. {
|
||||
self.0.push(PointCharge(pos.to_f64(), charge));
|
||||
self.0
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push(PointCharge(pos.to_f64(), charge));
|
||||
}
|
||||
}
|
||||
pub fn get_potential_energy_change(&self, pos: &BlockPos, charge: f64) -> f64 {
|
||||
@ -150,22 +167,23 @@ impl PotentialCalculator {
|
||||
return 0.;
|
||||
}
|
||||
let mut sum: f64 = 0.;
|
||||
for i in &self.0 {
|
||||
let charges = self.0.lock().unwrap();
|
||||
for i in charges.iter() {
|
||||
sum += i.get_potential_change(pos);
|
||||
}
|
||||
sum * charge
|
||||
}
|
||||
}
|
||||
|
||||
use crossbeam::atomic::AtomicCell;
|
||||
|
||||
pub struct SpawnState {
|
||||
spawnable_chunk_count: i32,
|
||||
pub mob_category_counts: MobCounts,
|
||||
spawn_potential: PotentialCalculator,
|
||||
local_mob_cap_calculator: LocalMobCapCalculator,
|
||||
// unmodifiable_mob_category_counts: MobCounts, seems only for debug
|
||||
last_checked_pos: BlockPos,
|
||||
last_checked_type: &'static EntityType,
|
||||
last_charge: f64,
|
||||
last_checked: AtomicCell<Option<(BlockPos, &'static EntityType, f64)>>,
|
||||
}
|
||||
|
||||
impl fmt::Debug for SpawnState {
|
||||
@ -175,9 +193,7 @@ impl fmt::Debug for SpawnState {
|
||||
.field("mob_category_counts", &self.mob_category_counts)
|
||||
.field("spawn_potential", &self.spawn_potential)
|
||||
.field("local_mob_cap_calculator", &self.local_mob_cap_calculator)
|
||||
.field("last_checked_pos", &self.last_checked_pos)
|
||||
.field("last_checked_type", &self.last_checked_type.resource_name)
|
||||
.field("last_charge", &self.last_charge)
|
||||
.field("last_checked", &self.last_checked)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
@ -188,9 +204,9 @@ impl SpawnState {
|
||||
entities: &ArcSwap<Vec<Arc<dyn EntityBase>>>,
|
||||
world: &Arc<World>,
|
||||
) -> Self {
|
||||
let mut potential = PotentialCalculator::default();
|
||||
let mut local_mob_cap = LocalMobCapCalculator::default();
|
||||
let mut counter = MobCounts::default();
|
||||
let potential = PotentialCalculator::default();
|
||||
let local_mob_cap = LocalMobCapCalculator::default();
|
||||
let counter = MobCounts::default();
|
||||
for entity in entities.load().iter() {
|
||||
let entity = entity.get_entity();
|
||||
let entity_type = entity.entity_type;
|
||||
@ -213,19 +229,16 @@ impl SpawnState {
|
||||
mob_category_counts: counter,
|
||||
spawn_potential: potential,
|
||||
local_mob_cap_calculator: local_mob_cap,
|
||||
// unmodifiable_mob_category_counts: counter,
|
||||
last_checked_pos: BlockPos::new(i32::MAX, i32::MAX, i32::MAX),
|
||||
last_checked_type: &EntityType::PLAYER,
|
||||
last_charge: 0.,
|
||||
last_checked: AtomicCell::new(None),
|
||||
}
|
||||
}
|
||||
#[inline]
|
||||
const fn can_spawn_for_category_global(&self, category: &'static MobCategory) -> bool {
|
||||
self.mob_category_counts.0[category.id]
|
||||
pub fn can_spawn_for_category_global(&self, category: &'static MobCategory) -> bool {
|
||||
self.mob_category_counts.0[category.id].load(Relaxed)
|
||||
< category.max * self.spawnable_chunk_count / MAGIC_NUMBER
|
||||
}
|
||||
fn can_spawn_for_category_local(
|
||||
&mut self,
|
||||
pub fn can_spawn_for_category_local(
|
||||
&self,
|
||||
world: &Arc<World>,
|
||||
category: &'static MobCategory,
|
||||
chunk_pos: Vector2<i32>,
|
||||
@ -233,44 +246,56 @@ impl SpawnState {
|
||||
self.local_mob_cap_calculator
|
||||
.can_spawn(category, world, chunk_pos)
|
||||
}
|
||||
async fn can_spawn(
|
||||
&mut self,
|
||||
pub async fn can_spawn(
|
||||
&self,
|
||||
entity_type: &'static EntityType,
|
||||
pos: &BlockPos,
|
||||
world: &Arc<World>,
|
||||
) -> bool {
|
||||
self.last_checked_pos = *pos;
|
||||
self.last_checked_type = entity_type;
|
||||
// TODO get biome
|
||||
let biome = world.level.get_rough_biome(pos).await;
|
||||
if let Some(cost) = biome.spawn_costs.get(entity_type.resource_name) {
|
||||
self.last_charge = cost.charge;
|
||||
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<World>,
|
||||
) {
|
||||
let charge;
|
||||
if self.last_checked_pos.eq(pos) && self.last_checked_type == entity_type {
|
||||
charge = self.last_charge;
|
||||
let charge = if let Some((l_pos, l_type, l_charge)) = self.last_checked.load()
|
||||
&& l_pos.eq(pos)
|
||||
&& l_type == entity_type
|
||||
{
|
||||
Some(l_charge)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let charge = if let Some(charge) = charge {
|
||||
charge
|
||||
} else {
|
||||
// TODO get biome
|
||||
let biome = world.level.get_rough_biome(pos).await;
|
||||
if let Some(cost) = biome.spawn_costs.get(entity_type.resource_name) {
|
||||
charge = cost.charge;
|
||||
} else {
|
||||
charge = 0.;
|
||||
}
|
||||
}
|
||||
biome
|
||||
.spawn_costs
|
||||
.get(entity_type.resource_name)
|
||||
.map_or(0., |cost| cost.charge)
|
||||
};
|
||||
|
||||
self.spawn_potential.add_charge(pos, charge);
|
||||
self.mob_category_counts.add(entity_type.category);
|
||||
@ -316,7 +341,7 @@ pub async fn spawn_for_chunk(
|
||||
world: &Arc<World>,
|
||||
chunk_pos: Vector2<i32>,
|
||||
chunk: &Arc<ChunkData>,
|
||||
spawn_state: &mut SpawnState,
|
||||
spawn_state: &SpawnState,
|
||||
spawn_list: &Vec<&'static MobCategory>,
|
||||
) {
|
||||
// debug!("spawn for chunk {:?}", chunk_pos);
|
||||
@ -354,7 +379,7 @@ pub async fn spawn_category_for_position(
|
||||
world: &Arc<World>,
|
||||
pos: BlockPos,
|
||||
chunk_pos: &Vector2<i32>,
|
||||
spawn_state: &mut SpawnState,
|
||||
spawn_state: &SpawnState,
|
||||
) {
|
||||
// TODO StructureManager structureManager = level.structureManager();
|
||||
// TODO blockState.isRedstoneConductor(chunk, pos) is true then return
|
||||
|
||||
Loading…
Reference in New Issue
Block a user