Skip to main content

steel_core/player/player_inventory/
player_handlers.rs

1use std::{f32::consts::TAU, mem, sync::Arc};
2
3use crate::{
4    entity::{Entity, LivingEntity as _, RemovalReason, entities::ItemEntity},
5    inventory::{
6        click::Click,
7        container::{Container, CraftingContainer, clear_or_count_matching_stack},
8        lock::{ContainerId, ContainerLockGuard},
9        menu::{
10            Menu,
11            kinds::{INVENTORY_MENU_CONTAINER_ID, InventoryKind},
12        },
13        slots::CraftingHandler,
14    },
15    player::{Player, connection::NetworkConnection as _},
16};
17use glam::DVec3;
18use steel_math::trig;
19use steel_protocol::packets::game::SSeenAdvancement::Opened;
20use steel_protocol::packets::game::{
21    CContainerClose, COpenScreen, CSetPlayerInventory, ClickType, SContainerButtonClick,
22    SContainerClick, SContainerClose, SContainerSlotStateChanged, SRenameItem, SSeenAdvancement,
23    SSetBeacon, SSetCarriedItem, SSetCreativeModeSlot, SoundSource,
24};
25use steel_registry::data_components::vanilla_components::SwingAnimation;
26use steel_registry::item_stack::ItemStack;
27use steel_registry::mob_effect::MobEffectRef;
28use steel_registry::sound_events;
29use steel_registry::stat::vanilla_stat_types;
30use steel_registry::vanilla_custom_stats;
31use steel_registry::{REGISTRY, RegistryExt};
32use steel_utils::{
33    Downcast as _,
34    locks::Shared,
35    translations,
36    types::{GameType, InteractionHand},
37};
38use text_components::TextComponent;
39
40use super::{
41    DeferredMenuAction, MenuItemDisposition, MenuOpenContext, MenuRemovalStatus, OpenMenuDispatch,
42    OpenMenuUnavailable, PendingMenuOpen, PlayerInventory, Prediction, PreparedMenu,
43    TerminalMenuRemoval,
44};
45
46impl Player {
47    fn take_open_menu_for_callback(
48        &self,
49        expected_container_id: Option<i32>,
50    ) -> Result<Menu, OpenMenuUnavailable> {
51        let mut open_menu = self.open_menu.lock();
52        if open_menu.dispatch.is_some() {
53            return Err(OpenMenuUnavailable::Unavailable);
54        }
55
56        let Some(menu) = open_menu.menu.as_ref() else {
57            return Err(OpenMenuUnavailable::Closed);
58        };
59        if expected_container_id.is_some_and(|expected| i32::from(menu.container_id()) != expected)
60        {
61            return Err(OpenMenuUnavailable::Unavailable);
62        }
63
64        let container_id = menu.container_id();
65        let overrides_player_slots = menu.overrides_player_slots();
66        let Some(menu) = open_menu.menu.take() else {
67            return Err(OpenMenuUnavailable::Unavailable);
68        };
69        open_menu.dispatch = Some(OpenMenuDispatch {
70            container_id,
71            overrides_player_slots,
72            actions: Vec::new(),
73        });
74        Ok(menu)
75    }
76
77    fn finish_open_menu_callback(&self, menu: Menu) {
78        let actions = {
79            let mut open_menu = self.open_menu.lock();
80            let Some(dispatch) = open_menu.dispatch.take() else {
81                open_menu.menu = Some(menu);
82                return;
83            };
84            if let Some(terminal_removal) = open_menu.terminal_removal.as_mut() {
85                Self::queue_deferred_menus(terminal_removal, dispatch.actions);
86                drop(open_menu);
87                self.finish_terminal_menu_main_cleanup(Some(menu));
88                return;
89            }
90            open_menu.menu = Some(menu);
91            open_menu.active_open_operations += 1;
92            dispatch.actions
93        };
94
95        self.run_deferred_menu_actions(actions);
96    }
97
98    fn finish_open_menu_removal(&self) {
99        let actions = {
100            let mut open_menu = self.open_menu.lock();
101            let Some(dispatch) = open_menu.dispatch.take() else {
102                return;
103            };
104            if let Some(terminal_removal) = open_menu.terminal_removal.as_mut() {
105                Self::queue_deferred_menus(terminal_removal, dispatch.actions);
106                drop(open_menu);
107                self.finish_terminal_menu_main_cleanup(None);
108                return;
109            }
110            open_menu.active_open_operations += 1;
111            dispatch.actions
112        };
113
114        self.run_deferred_menu_actions(actions);
115    }
116
117    fn run_deferred_menu_actions(&self, actions: Vec<DeferredMenuAction>) {
118        for action in actions {
119            match action {
120                DeferredMenuAction::Close { send_packet } => {
121                    if send_packet {
122                        self.close_container();
123                    } else {
124                        self.do_close_container();
125                    }
126                }
127                DeferredMenuAction::Open(prepared) => {
128                    self.execute_menu_open(*prepared);
129                }
130                DeferredMenuAction::Install(prepared) => {
131                    let PreparedMenu { title, menu } = *prepared;
132                    self.open_prepared_menu(title, menu);
133                }
134            }
135        }
136
137        self.finish_menu_open_operation();
138    }
139
140    fn queue_deferred_menus(
141        terminal_removal: &mut TerminalMenuRemoval,
142        actions: Vec<DeferredMenuAction>,
143    ) {
144        terminal_removal
145            .pending_menus
146            .extend(actions.into_iter().filter_map(|action| match action {
147                DeferredMenuAction::Install(prepared) => Some(prepared.menu),
148                DeferredMenuAction::Close { .. } | DeferredMenuAction::Open(_) => None,
149            }));
150    }
151
152    fn begin_menu_open_operation(&self) -> bool {
153        let mut open_menu = self.open_menu.lock();
154        if open_menu.terminal_removal.is_some() {
155            return false;
156        }
157        open_menu.active_open_operations += 1;
158        true
159    }
160
161    fn finish_menu_open_operation(&self) {
162        {
163            let mut open_menu = self.open_menu.lock();
164            debug_assert!(open_menu.active_open_operations > 0);
165            open_menu.active_open_operations -= 1;
166        }
167        self.try_finish_terminal_menu_removal();
168    }
169
170    /// Attempts to pick up nearby item entities.
171    ///
172    /// Mirrors vanilla's `Player.aiStep()` item pickup logic:
173    /// - Calculates pickup area as bounding box inflated by (1.0, 0.5, 1.0)
174    /// - Calls `playerTouch()` on each entity in range
175    pub(in crate::player) fn touch_nearby_items(&self) {
176        if self.game_mode() == GameType::Spectator {
177            return;
178        }
179
180        let pickup_area = self.bounding_box().inflate_xyz(1.0, 0.5, 1.0);
181        let world = self.get_world();
182        let entities = world.get_entities_in_aabb(&pickup_area);
183
184        let Some(player_arc) = world.players.get_by_entity_id(self.id()) else {
185            return;
186        };
187
188        for entity in entities {
189            if entity.id() == self.id() || entity.is_removed() {
190                continue;
191            }
192
193            entity.player_touch(&player_arc);
194        }
195    }
196
197    /// Resolves an optional beacon effect ID, rejecting IDs vanilla's packet codec would reject.
198    pub(super) fn resolve_beacon_effect(id: Option<i32>) -> Result<Option<MobEffectRef>, ()> {
199        id.map(|id| {
200            let id = usize::try_from(id).map_err(|_| ())?;
201            REGISTRY.mob_effects.by_id(id).ok_or(())
202        })
203        .transpose()
204    }
205
206    /// Handles a beacon effect selection from the set-beacon packet.
207    pub fn handle_set_beacon_packet(&self, packet: SSetBeacon) {
208        let (Ok(primary), Ok(secondary)) = (
209            Self::resolve_beacon_effect(packet.primary),
210            Self::resolve_beacon_effect(packet.secondary),
211        ) else {
212            log::warn!(
213                "Player {} sent an unknown beacon effect id",
214                self.gameprofile.name()
215            );
216            self.disconnect(translations::MULTIPLAYER_DISCONNECT_GENERIC.msg());
217            return;
218        };
219
220        let Ok(mut menu) = self.take_open_menu_for_callback(None) else {
221            return;
222        };
223        if !menu.still_valid(self) {
224            log::debug!(
225                "Player {} interacted with invalid menu",
226                self.gameprofile.name()
227            );
228            self.finish_open_menu_callback(menu);
229            return;
230        }
231        if !menu.update_effects(primary, secondary, &self.connection) {
232            log::warn!(
233                "Player {} tried to set invalid beacon effects",
234                self.gameprofile.name()
235            );
236            self.finish_open_menu_callback(menu);
237            self.disconnect(translations::MULTIPLAYER_DISCONNECT_GENERIC.msg());
238            return;
239        }
240        self.finish_open_menu_callback(menu);
241    }
242
243    /// Dispatches a container button click to the open menu.
244    pub fn handle_container_button_click(&self, packet: SContainerButtonClick) {
245        self.reset_last_action_time();
246        match self.take_open_menu_for_callback(Some(packet.container_id)) {
247            Ok(mut menu) => {
248                if self.game_mode() == GameType::Spectator {
249                    // Spectator button clicks are ignored without resynchronizing the menu.
250                } else if !menu.still_valid(self) {
251                    log::debug!(
252                        "Player {} interacted with invalid menu {}",
253                        self.gameprofile.name(),
254                        packet.container_id
255                    );
256                } else if menu.click_menu_button(packet.button_id, self) {
257                    menu.behavior_mut().broadcast_changes(&self.connection);
258                }
259                self.finish_open_menu_callback(menu);
260            }
261            Err(OpenMenuUnavailable::Closed) => {
262                log::debug!("container button click without an open menu");
263            }
264            Err(OpenMenuUnavailable::Unavailable) => {}
265        }
266    }
267
268    /// Handles a container click packet (slot interaction).
269    pub fn handle_container_click(&self, packet: SContainerClick) {
270        self.reset_last_action_time();
271        match self.take_open_menu_for_callback(Some(packet.container_id)) {
272            Ok(mut menu) => {
273                self.process_container_click(&mut menu, packet);
274                self.finish_open_menu_callback(menu);
275            }
276            Err(OpenMenuUnavailable::Closed) => {
277                let mut menu = self.inventory_menu.lock();
278                if i32::from(menu.behavior().container_id()) == packet.container_id {
279                    self.process_container_click(&mut menu, packet);
280                }
281            }
282            Err(OpenMenuUnavailable::Unavailable) => {}
283        }
284    }
285
286    /// Processes a container click on any menu implementing the Menu trait.
287    ///
288    /// This is the common implementation shared between inventory menu and
289    /// external menus (crafting table, chest, etc.).
290    fn process_container_click(&self, menu: &mut Menu, packet: SContainerClick) {
291        if self.game_mode() == GameType::Spectator || self.get_health() <= 0.0 {
292            menu.behavior_mut()
293                .send_all_data_to_remote(&self.connection);
294            return;
295        }
296
297        if !menu.still_valid(self) {
298            log::debug!(
299                "Player {} interacted with invalid menu",
300                self.gameprofile.name()
301            );
302            return;
303        }
304
305        // Vanilla rejects positive out-of-range slots before applying client
306        // prediction hashes or resynchronizing the menu. Its validity check
307        // admits every negative slot because each is less than the slot count.
308        let slot_count = menu.behavior().slot_count();
309        let packet_slot_is_valid = packet.slot_num < 0
310            || usize::try_from(packet.slot_num).is_ok_and(|slot| slot < slot_count);
311        if !packet_slot_is_valid {
312            log::debug!(
313                "Player {} clicked invalid slot index: {}, available slots: {}",
314                self.gameprofile.name(),
315                packet.slot_num,
316                slot_count
317            );
318            return;
319        }
320
321        // Parse and validate the remaining raw click fields once. A malformed
322        // button or drag encoding is not applied, but the state sync below
323        // still runs so the client's prediction gets corrected.
324        let click = Click::parse(
325            packet.slot_num,
326            packet.button_num,
327            packet.click_type,
328            slot_count,
329        );
330        if click.is_none() {
331            log::debug!(
332                "Player {} sent malformed container click (slot {}, button {}, {:?})",
333                self.gameprofile.name(),
334                packet.slot_num,
335                packet.button_num,
336                packet.click_type
337            );
338            // Vanilla rejects positive out-of-range slots before `doClick`.
339            // Once admitted, any non-QuickCraft input cancels an active drag,
340            // and malformed QuickCraft headers/types reset their own state.
341            let quick_craft_header = packet.button_num & 3;
342            let quick_craft_type = (packet.button_num >> 2) & 3;
343            if packet.click_type != ClickType::QuickCraft
344                || quick_craft_header == 3
345                || (quick_craft_header == 0 && quick_craft_type == 3)
346            {
347                menu.behavior_mut().reset_quick_craft();
348            }
349        }
350
351        let full_resync_needed = packet.state_id as u32 != menu.behavior().state_id();
352
353        menu.behavior_mut().suppress_remote_updates();
354
355        if let Some(click) = click {
356            menu.clicked(click, self);
357        }
358
359        for (slot, hash) in packet.changed_slots {
360            let slot = slot as usize;
361            // Result/fake slots are server-authoritative (their contents are
362            // recomputed from a recipe). Don't let the client's prediction set
363            // our view of what it knows, or `broadcast_changes` will think the
364            // client already has the freshly-crafted result and skip syncing it
365            // — leaving the slot blank until the next click forces a resend.
366            if menu
367                .behavior()
368                .slots()
369                .get(slot)
370                .is_some_and(|slot| slot.is_fake())
371            {
372                menu.behavior_mut().mark_remote_slot_unknown(slot);
373                continue;
374            }
375            menu.behavior_mut().set_remote_slot(slot, hash);
376        }
377
378        menu.behavior_mut().set_remote_carried(packet.carried_item);
379        menu.behavior_mut().resume_remote_updates();
380
381        if full_resync_needed {
382            menu.behavior_mut()
383                .send_all_data_to_remote(&self.connection);
384        } else {
385            menu.behavior_mut().broadcast_changes(&self.connection);
386        }
387    }
388
389    /// Handles a container close packet.
390    ///
391    /// Based on Java's `ServerGamePacketListenerImpl::handleContainerClose`.
392    pub fn handle_container_close(&self, packet: SContainerClose) {
393        log::debug!(
394            "Player {} closed container {}",
395            self.gameprofile.name(),
396            packet.container_id
397        );
398
399        let open_menu = self.open_menu.lock();
400        let closes_open_menu = open_menu
401            .menu
402            .as_ref()
403            .is_some_and(|menu| i32::from(menu.container_id()) == packet.container_id)
404            || open_menu
405                .dispatch
406                .as_ref()
407                .is_some_and(|dispatch| i32::from(dispatch.container_id) == packet.container_id);
408        drop(open_menu);
409
410        if closes_open_menu {
411            self.do_close_container();
412            return;
413        }
414
415        if packet.container_id == i32::from(INVENTORY_MENU_CONTAINER_ID) {
416            let mut menu = self.inventory_menu.lock();
417            menu.removed(self);
418        }
419    }
420
421    /// Handles an anvil rename packet.
422    pub fn handle_rename_item(self: &Arc<Self>, packet: SRenameItem) {
423        match self.take_open_menu_for_callback(None) {
424            Ok(mut menu) => {
425                if menu.still_valid(self) {
426                    menu.set_item_name(packet.name, self);
427                }
428                self.finish_open_menu_callback(menu);
429            }
430            Err(OpenMenuUnavailable::Closed) => {
431                log::debug!("rename item without an open menu");
432            }
433            Err(OpenMenuUnavailable::Unavailable) => {}
434        }
435    }
436
437    /// Handles a container slot state changed packet (e.g., crafter slot toggle).
438    pub fn handle_container_slot_state_changed(&self, packet: SContainerSlotStateChanged) {
439        log::debug!(
440            "Player {} changed slot {} state to {} in container {}",
441            self.gameprofile.name(),
442            packet.slot_id,
443            packet.new_state,
444            packet.container_id
445        );
446        // TODO: Implement slot state change handling
447        // This is used for the crafter block to enable/disable slots
448    }
449
450    /// Handles a creative mode slot set packet.
451    pub fn handle_set_creative_mode_slot(&self, packet: SSetCreativeModeSlot) {
452        if self.game_mode() != GameType::Creative {
453            return;
454        }
455
456        let drop = packet.slot_num < 0;
457        let item_stack = packet.item_stack;
458
459        let valid_slot = packet.slot_num >= 1 && packet.slot_num <= 45;
460        let valid_data = item_stack.is_empty() || item_stack.count <= item_stack.max_stack_size();
461
462        if valid_slot && valid_data {
463            let mut menu = self.inventory_menu.lock();
464            let slot_index = packet.slot_num as usize;
465
466            menu.set_slot_by_player(slot_index, item_stack.clone(), self);
467            if (1..=4).contains(&slot_index) {
468                menu.update_crafting_result();
469            }
470            menu.behavior_mut()
471                .set_remote_slot_known(slot_index, &item_stack);
472            menu.behavior_mut().broadcast_changes(&self.connection);
473        } else if drop && valid_data {
474            {
475                let mut throttler = self.session.drop_spam_throttler.lock();
476                if throttler.is_under_threshold() {
477                    throttler.increment();
478                } else {
479                    log::warn!(
480                        "Player {} was dropping items too fast in creative mode; ignoring",
481                        self.gameprofile.name(),
482                    );
483                    return;
484                }
485            }
486            let _ = self.drop_item(item_stack, true, Prediction::Predicted);
487        }
488    }
489
490    /// Sets selected slot
491    pub fn handle_set_carried_item(&self, packet: SSetCarriedItem) {
492        if self
493            .inventory
494            .lock()
495            .try_set_selected_slot_from_packet(packet.slot)
496            .is_err()
497        {
498            log::warn!(
499                "{} tried to set an invalid carried item",
500                self.gameprofile.name()
501            );
502        } else {
503            self.reset_last_action_time();
504        }
505    }
506
507    /// Set the selected advancement
508    pub fn handle_seen_advancement(&self, packet: SSeenAdvancement) {
509        if let Opened(tab_id) = packet {
510            let advancement = REGISTRY.advancements.value_by_key(&tab_id);
511            if advancement.is_some() {
512                self.advancements.lock().set_selected_tab(self, advancement);
513            }
514        }
515    }
516
517    /// Sends all inventory slots to the client (full sync).
518    /// This should be called when the player first joins.
519    pub fn send_inventory_to_remote(&self) {
520        self.inventory_menu
521            .lock()
522            .behavior_mut()
523            .send_all_data_to_remote(&self.connection);
524    }
525
526    /// Generates the next container ID (1-100, wrapping around).
527    ///
528    /// Based on Java's `ServerPlayer::nextContainerCounter`.
529    fn next_container_counter(&self) -> u8 {
530        self.container_counter.lock().next()
531    }
532
533    /// Opens a menu for this player.
534    ///
535    /// Based on Java's `ServerPlayer::openMenu`.
536    ///
537    /// # Arguments
538    /// * `title` - The display title shown in the open-screen packet.
539    /// * `create` - Factory invoked with the allocated container id, player,
540    ///   and current world. If called by a menu hook, the factory runs after
541    ///   that hook releases its container locks.
542    ///
543    /// # Panics
544    /// Panics if the created menu uses a different container id than the one
545    /// allocated for it, or has no menu type (i.e. the player's own inventory
546    /// menu, which must never be opened via `open_menu`).
547    pub fn open_menu(
548        &self,
549        title: impl Into<TextComponent>,
550        create: impl for<'a> FnOnce(MenuOpenContext<'a>) -> Menu + Send + 'static,
551    ) {
552        if !self.begin_menu_open_operation() {
553            return;
554        }
555        self.open_menu_inner(PendingMenuOpen {
556            title: title.into(),
557            create: Box::new(create),
558        });
559        self.finish_menu_open_operation();
560    }
561
562    fn open_menu_inner(&self, pending: PendingMenuOpen) {
563        self.do_close_container();
564
565        let mut open_menu = self.open_menu.lock();
566        if open_menu.terminal_removal.is_some() {
567            return;
568        }
569        if let Some(dispatch) = open_menu.dispatch.as_mut() {
570            dispatch
571                .actions
572                .push(DeferredMenuAction::Open(Box::new(pending)));
573            return;
574        }
575        drop(open_menu);
576
577        self.execute_menu_open(pending);
578    }
579
580    fn execute_menu_open(&self, pending: PendingMenuOpen) {
581        {
582            let mut open_menu = self.open_menu.lock();
583            if open_menu.terminal_removal.is_some() {
584                return;
585            }
586            if let Some(dispatch) = open_menu.dispatch.as_mut() {
587                dispatch
588                    .actions
589                    .push(DeferredMenuAction::Open(Box::new(pending)));
590                return;
591            }
592        }
593
594        let PendingMenuOpen { title, create } = pending;
595        let container_id = self.next_container_counter();
596        let world = self.get_world();
597        let menu = create(MenuOpenContext {
598            container_id,
599            player: self,
600            world: &world,
601        });
602        assert_eq!(
603            menu.container_id(),
604            container_id,
605            "open_menu factory returned container id {}, but {} was allocated",
606            menu.container_id(),
607            container_id,
608        );
609        self.open_prepared_menu(title, menu);
610    }
611
612    fn open_prepared_menu(&self, title: TextComponent, mut menu: Menu) {
613        loop {
614            {
615                let mut open_menu = self.open_menu.lock();
616                if let Some(terminal_removal) = open_menu.terminal_removal.as_mut() {
617                    terminal_removal.pending_menus.push(menu);
618                    return;
619                }
620            }
621
622            // A removal hook may have opened another menu while the initiating
623            // open call was closing its predecessor.
624            self.do_close_container();
625
626            let mut open_menu = self.open_menu.lock();
627            if let Some(terminal_removal) = open_menu.terminal_removal.as_mut() {
628                terminal_removal.pending_menus.push(menu);
629                return;
630            }
631            if let Some(dispatch) = open_menu.dispatch.as_mut() {
632                dispatch
633                    .actions
634                    .push(DeferredMenuAction::Install(Box::new(PreparedMenu {
635                        title,
636                        menu,
637                    })));
638                return;
639            }
640            if open_menu.menu.is_some() {
641                continue;
642            }
643            open_menu.dispatch = Some(OpenMenuDispatch {
644                container_id: menu.container_id(),
645                overrides_player_slots: menu.overrides_player_slots(),
646                actions: Vec::new(),
647            });
648            break;
649        }
650
651        self.send_packet(COpenScreen {
652            container_id: i32::from(menu.container_id()),
653            menu_type: menu
654                .menu_type()
655                .expect("a menu opened via open_menu must declare a menu type"),
656            title,
657        });
658
659        // Fire on_open before the full sync so anything the menu populates here
660        // is included in the first render sent below.
661        menu.on_open(self);
662
663        menu.behavior_mut()
664            .send_all_data_to_remote(&self.connection);
665
666        self.finish_open_menu_callback(menu);
667    }
668
669    /// A shared handle to the 2x2 crafting grid of the always-open inventory
670    /// menu.
671    pub fn crafting_container(&self) -> Shared<CraftingContainer> {
672        let menu = self.inventory_menu.lock();
673        let Some(kind) = menu.kind().downcast_ref::<InventoryKind>() else {
674            unreachable!("a player's inventory_menu is always the Inventory kind");
675        };
676        kind.crafting_container()
677    }
678
679    /// A shared handler for the 2x2 crafting grid of the always-open inventory
680    /// menu and its result.
681    pub(crate) fn inventory_crafting_handler(&self) -> CraftingHandler {
682        let menu = self.inventory_menu.lock();
683        let Some(kind) = menu.kind().downcast_ref::<InventoryKind>() else {
684            unreachable!("a player's inventory_menu is always the Inventory kind");
685        };
686        kind.crafting_handler()
687    }
688
689    /// Closes the currently open container and returns to the inventory menu.
690    ///
691    /// Based on Java's `ServerPlayer::closeContainer`.
692    /// This sends a close packet to the client.
693    pub fn close_container(&self) {
694        self.close_open_menu(true);
695    }
696
697    /// Internal close container logic without sending a packet.
698    ///
699    /// Based on Java's `ServerPlayer::doCloseContainer`.
700    /// Called when the client sends a close packet or when opening a new menu.
701    pub fn do_close_container(&self) {
702        self.close_open_menu(false);
703    }
704
705    /// Removes both the base inventory menu and any external menu.
706    ///
707    /// This mirrors `Player::remove`: base crafting and carried items are
708    /// handled before the external menu, and menu hooks cannot install a
709    /// replacement while removal is in progress. The inventory menu remains
710    /// reusable because Steel keeps one `Player` across world changes.
711    pub fn remove_all_menus(&self) -> MenuRemovalStatus {
712        self.remove_all_menus_with_disposition(self.default_menu_item_disposition())
713    }
714
715    pub(in crate::player) fn remove_all_menus_with_disposition(
716        &self,
717        disposition: MenuItemDisposition,
718    ) -> MenuRemovalStatus {
719        let menu = {
720            let mut open_menu = self.open_menu.lock();
721            if let Some(terminal_removal) = open_menu.terminal_removal.as_mut() {
722                terminal_removal.disposition = terminal_removal.disposition.combine(disposition);
723                return MenuRemovalStatus::Pending;
724            }
725
726            open_menu.terminal_removal = Some(TerminalMenuRemoval {
727                disposition,
728                main_cleanup_complete: false,
729                pending_cleanup_in_progress: false,
730                pending_menus: Vec::new(),
731            });
732            if open_menu.dispatch.is_some() {
733                return MenuRemovalStatus::Pending;
734            }
735
736            open_menu.menu.take()
737        };
738
739        self.finish_terminal_menu_main_cleanup(menu);
740        if self.open_menu.lock().terminal_removal.is_none() {
741            MenuRemovalStatus::Complete
742        } else {
743            MenuRemovalStatus::Pending
744        }
745    }
746
747    fn finish_terminal_menu_main_cleanup(&self, mut menu: Option<Menu>) {
748        self.inventory_menu.lock().removed(self);
749        if let Some(menu) = menu.as_mut() {
750            self.remove_open_menu(menu);
751        }
752
753        {
754            let mut open_menu = self.open_menu.lock();
755            let Some(terminal_removal) = open_menu.terminal_removal.as_mut() else {
756                return;
757            };
758            terminal_removal.main_cleanup_complete = true;
759        }
760        self.try_finish_terminal_menu_removal();
761    }
762
763    fn try_finish_terminal_menu_removal(&self) {
764        loop {
765            let pending_menus = {
766                let mut open_menu = self.open_menu.lock();
767                if open_menu.active_open_operations != 0 {
768                    return;
769                }
770                let Some(terminal_removal) = open_menu.terminal_removal.as_mut() else {
771                    return;
772                };
773                if !terminal_removal.main_cleanup_complete {
774                    return;
775                }
776                if terminal_removal.pending_cleanup_in_progress {
777                    return;
778                }
779                if terminal_removal.pending_menus.is_empty() {
780                    open_menu.terminal_removal = None;
781                    debug_assert!(open_menu.menu.is_none());
782                    return;
783                }
784                terminal_removal.pending_cleanup_in_progress = true;
785                mem::take(&mut terminal_removal.pending_menus)
786            };
787
788            for mut pending_menu in pending_menus {
789                pending_menu.removed(self);
790            }
791
792            let mut open_menu = self.open_menu.lock();
793            let Some(terminal_removal) = open_menu.terminal_removal.as_mut() else {
794                return;
795            };
796            terminal_removal.pending_cleanup_in_progress = false;
797        }
798    }
799
800    #[cfg(test)]
801    pub(in crate::player) fn retry_terminal_menu_removal_for_test(&self) {
802        self.try_finish_terminal_menu_removal();
803    }
804
805    fn close_open_menu(&self, send_packet: bool) {
806        let menu = {
807            let mut open_menu = self.open_menu.lock();
808            if open_menu.terminal_removal.is_some() {
809                return;
810            }
811            if let Some(dispatch) = open_menu.dispatch.as_mut() {
812                dispatch
813                    .actions
814                    .push(DeferredMenuAction::Close { send_packet });
815                return;
816            }
817            let Some(menu) = open_menu.menu.take() else {
818                return;
819            };
820            open_menu.dispatch = Some(OpenMenuDispatch {
821                container_id: menu.container_id(),
822                overrides_player_slots: menu.overrides_player_slots(),
823                actions: Vec::new(),
824            });
825            menu
826        };
827
828        let mut menu = menu;
829        if send_packet {
830            self.send_packet(CContainerClose {
831                container_id: i32::from(menu.container_id()),
832            });
833        }
834        self.remove_open_menu(&mut menu);
835        self.finish_open_menu_removal();
836    }
837
838    fn remove_open_menu(&self, menu: &mut Menu) {
839        let overrides_player_slots = menu.overrides_player_slots();
840        menu.removed(self);
841        if overrides_player_slots {
842            self.request_inventory_resync(0..PlayerInventory::INVENTORY_SIZE);
843        } else {
844            self.inventory_menu
845                .lock()
846                .behavior_mut()
847                .transfer_state(menu.behavior());
848        }
849    }
850
851    /// Returns true if the player has an external menu open (not the inventory).
852    #[must_use]
853    pub fn has_container_open(&self) -> bool {
854        let open_menu = self.open_menu.lock();
855        open_menu.menu.is_some() || open_menu.dispatch.is_some()
856    }
857
858    /// Runs the open menu's per-tick hook, if an external menu is open.
859    ///
860    /// Scoped to the opened menu; the base inventory menu is not ticked. Called
861    /// once per player tick, before syncing inventory changes to the client.
862    pub fn tick_open_menu(&self) {
863        let Ok(mut menu) = self.take_open_menu_for_callback(None) else {
864            return;
865        };
866        if !menu.still_valid(self) {
867            self.close_container();
868            self.finish_open_menu_callback(menu);
869            return;
870        }
871        menu.on_tick(self);
872        self.finish_open_menu_callback(menu);
873    }
874
875    /// Broadcasts inventory changes to the client (incremental sync).
876    /// This is called every tick to sync only changed slots.
877    pub fn broadcast_inventory_changes(&self) {
878        let mut open_menu = self.open_menu.lock();
879        if let Some(menu) = open_menu.menu.as_mut() {
880            menu.behavior_mut().broadcast_changes(&self.connection);
881            return;
882        }
883        if open_menu.dispatch.is_none() {
884            drop(open_menu);
885            self.inventory_menu
886                .lock()
887                .behavior_mut()
888                .broadcast_changes(&self.connection);
889        }
890    }
891
892    /// Requests direct synchronization of logical player-inventory slots.
893    pub(crate) fn request_inventory_resync(&self, slots: impl IntoIterator<Item = usize>) {
894        self.inventory_sync.lock().request(slots);
895    }
896
897    /// Sends the latest values for requested logical inventory slots.
898    pub(in crate::player) fn flush_inventory_resync(&self) {
899        let overrides_player_slots = {
900            let open_menu = self.open_menu.lock();
901            open_menu
902                .menu
903                .as_ref()
904                .is_some_and(Menu::overrides_player_slots)
905                || open_menu
906                    .dispatch
907                    .as_ref()
908                    .is_some_and(|dispatch| dispatch.overrides_player_slots)
909        };
910        let slots = self
911            .inventory_sync
912            .lock()
913            .take_ready(overrides_player_slots);
914        if slots.is_empty() {
915            return;
916        }
917
918        let packets = {
919            let inventory = self.inventory.lock();
920            slots
921                .into_iter()
922                .map(|slot| CSetPlayerInventory {
923                    slot: slot as i32,
924                    item_stack: inventory.get_item(slot).clone(),
925                })
926                .collect::<Vec<_>>()
927        };
928        for packet in packets {
929            self.send_packet(packet);
930        }
931    }
932
933    /// Removes or counts matching stacks across every location used by vanilla `/clear`.
934    pub(crate) fn clear_or_count_matching_items(
935        &self,
936        predicate: &dyn Fn(&ItemStack) -> bool,
937        amount_to_remove: i32,
938    ) -> i32 {
939        let counting_only = amount_to_remove == 0;
940        let mut count = self.inventory.lock().clear_or_count_matching_items(
941            predicate,
942            amount_to_remove,
943            counting_only,
944        );
945
946        count += self.inventory_menu.lock().clear_or_count_crafting_items(
947            predicate,
948            amount_to_remove - count,
949            counting_only,
950        );
951
952        let has_open_menu = {
953            let mut open_menu = self.open_menu.lock();
954            if let Some(menu) = open_menu.menu.as_mut() {
955                let behavior = menu.behavior_mut();
956                count += clear_or_count_matching_stack(
957                    behavior.carried_mut(),
958                    predicate,
959                    amount_to_remove - count,
960                    counting_only,
961                );
962                if behavior.carried().is_empty() {
963                    *behavior.carried_mut() = ItemStack::empty();
964                }
965                true
966            } else {
967                open_menu.dispatch.is_some()
968            }
969        };
970        if !has_open_menu {
971            let mut inventory_menu = self.inventory_menu.lock();
972            let behavior = inventory_menu.behavior_mut();
973            count += clear_or_count_matching_stack(
974                behavior.carried_mut(),
975                predicate,
976                amount_to_remove - count,
977                counting_only,
978            );
979            if behavior.carried().is_empty() {
980                *behavior.carried_mut() = ItemStack::empty();
981            }
982        }
983
984        self.inventory_menu.lock().update_crafting_result();
985        self.broadcast_inventory_changes();
986        count
987    }
988
989    /// Drops an item from the player's selected hotbar slot.
990    ///
991    /// Based on Java's `ServerPlayer.drop(boolean all)`.
992    ///
993    /// - `all`: If true, drops the entire stack (Ctrl+Q). If false, drops one item (Q).
994    pub fn drop_from_selected(&self, all: bool) {
995        if !self.can_drop_items() {
996            return;
997        }
998
999        let removed = {
1000            let mut inventory = self.inventory.lock();
1001            let selected_count = inventory.get_selected_item().count();
1002            inventory.split_item_in_hand(
1003                InteractionHand::MainHand,
1004                if all { selected_count } else { 1 },
1005            )
1006        };
1007
1008        let _ = self.drop_item(removed, true, Prediction::Predicted);
1009        self.reset_attack_strength_ticker();
1010    }
1011
1012    /// Swings the main hand and throws `item` in the facing direction.
1013    ///
1014    /// Based on Java's `LivingEntity.drop(ItemStack, boolean thrownFromHand, Prediction)`.
1015    /// `prediction` decides whether the swing is also sent to this player.
1016    #[must_use]
1017    pub fn drop_item(
1018        &self,
1019        item: ItemStack,
1020        thrown_from_hand: bool,
1021        prediction: Prediction,
1022    ) -> Option<Arc<ItemEntity>> {
1023        if item.is_empty() {
1024            return None;
1025        }
1026
1027        self.swing(
1028            InteractionHand::MainHand,
1029            SwingAnimation::DEFAULT,
1030            prediction == Prediction::ServerOnly,
1031        );
1032        self.spawn_dropped_item(item, false, thrown_from_hand)
1033    }
1034
1035    /// Spawns `item` as a dropped entity without swinging.
1036    ///
1037    /// Based on Java's `LivingEntity.createItemStackToDrop`, used directly by
1038    /// death drops and `/give` overflow.
1039    ///
1040    /// - `throw_randomly`: If true, the item is thrown in a random direction.
1041    ///   If false, it's thrown in the direction the player is facing.
1042    /// - `thrown_from_hand`: If true, sets the thrower and uses a longer pickup delay.
1043    #[must_use]
1044    pub fn spawn_dropped_item(
1045        &self,
1046        item: ItemStack,
1047        throw_randomly: bool,
1048        thrown_from_hand: bool,
1049    ) -> Option<Arc<ItemEntity>> {
1050        if item.is_empty() {
1051            return None;
1052        }
1053
1054        let pos = self.position();
1055        let (yaw, pitch) = self.rotation();
1056
1057        let spawn_y = self.get_eye_y() - 0.3;
1058
1059        let velocity = if throw_randomly {
1060            let power = rand::random_range(0.0..0.5);
1061            let angle = rand::random_range(0.0..TAU);
1062            DVec3::new(
1063                f64::from(-trig::sin(angle) * power),
1064                0.2,
1065                f64::from(trig::cos(angle) * power),
1066            )
1067        } else {
1068            let pitch_rad = pitch.to_radians();
1069            let yaw_rad = yaw.to_radians();
1070
1071            let sin_pitch = trig::sin(pitch_rad);
1072            let cos_pitch = trig::cos(pitch_rad);
1073            let sin_yaw = trig::sin(yaw_rad);
1074            let cos_yaw = trig::cos(yaw_rad);
1075
1076            let angle_offset = rand::random_range(0.0..TAU);
1077            let power_offset = rand::random_range(0.0..0.02);
1078
1079            DVec3::new(
1080                f64::from(-sin_yaw * cos_pitch * 0.3)
1081                    + f64::from(angle_offset.cos() * power_offset),
1082                f64::from(-sin_pitch * 0.3 + 0.1)
1083                    + f64::from((rand::random::<f32>() - rand::random::<f32>()) * 0.1),
1084                f64::from(cos_yaw * cos_pitch * 0.3) + f64::from(angle_offset.sin() * power_offset),
1085            )
1086        };
1087
1088        let spawn_pos = DVec3::new(pos.x, spawn_y, pos.z);
1089
1090        let item_ref = item.item;
1091        let item_count = item.count;
1092
1093        let entity = self
1094            .get_world()
1095            .spawn_item_with_velocity(spawn_pos, item, velocity)?;
1096        entity.set_pickup_delay(40);
1097        if thrown_from_hand {
1098            entity.set_thrower(self.gameprofile.id());
1099            self.award_stat_with_count(&vanilla_stat_types::ITEM_DROPPED, item_ref, item_count);
1100            self.award_custom_stat(&vanilla_custom_stats::DROP);
1101        }
1102        Some(entity)
1103    }
1104
1105    /// Returns true if the player can drop items.
1106    ///
1107    /// Based on Java's `Player.canDropItems()`.
1108    /// Returns false if the player is dead, removed, or has a flag preventing item drops.
1109    #[must_use]
1110    pub fn can_drop_items(&self) -> bool {
1111        Entity::is_alive(self)
1112    }
1113
1114    /// Returns whether items from a closing menu (crafting grid, anvil inputs,
1115    /// cursor) should be placed back into the inventory instead of dropped into
1116    /// the world.
1117    ///
1118    /// Matches vanilla's `AbstractContainerMenu.dropOrPlaceInInventory`: a
1119    /// disconnected player or one removed for any reason except a world change
1120    /// drops the items.
1121    #[must_use]
1122    pub fn returns_menu_items_to_inventory(&self) -> bool {
1123        let terminal_disposition = self
1124            .open_menu
1125            .lock()
1126            .terminal_removal
1127            .as_ref()
1128            .map(|terminal_removal| terminal_removal.disposition);
1129        if let Some(disposition) = terminal_disposition {
1130            return disposition == MenuItemDisposition::ReturnToInventory;
1131        }
1132
1133        self.default_menu_item_disposition() == MenuItemDisposition::ReturnToInventory
1134    }
1135
1136    fn default_menu_item_disposition(&self) -> MenuItemDisposition {
1137        let removed_outside_world_change =
1138            self.is_removed() && self.removal_reason() != Some(RemovalReason::ChangedWorld);
1139        if removed_outside_world_change || self.connection.closed() || self.get_health() <= 0.0 {
1140            MenuItemDisposition::Drop
1141        } else {
1142            MenuItemDisposition::ReturnToInventory
1143        }
1144    }
1145
1146    /// Tries to add an item to the player's inventory, playing the vanilla item-pickup
1147    /// sound when the entire stack fits.
1148    ///
1149    /// Returns true if the whole stack was added; otherwise the stack keeps the remainder.
1150    /// Mirrors the shared vanilla `Inventory.add` + `SoundEvents.ITEM_PICKUP` flow used by
1151    /// `/give` and advancement rewards: the sound only plays once the stack is fully consumed.
1152    pub fn add_item_with_sound(&self, stack: &mut ItemStack) -> bool {
1153        let added = self.inventory.lock().add(stack);
1154        if added {
1155            let pitch = ((rand::random::<f32>() - rand::random::<f32>()) * 0.7 + 1.0) * 2.0;
1156            self.get_world().play_sound_at(
1157                &sound_events::ENTITY_ITEM_PICKUP,
1158                SoundSource::Players,
1159                self.position(),
1160                0.2,
1161                pitch,
1162                None,
1163            );
1164        }
1165        added
1166    }
1167
1168    /// Tries to add an item to the player's inventory, dropping it if it doesn't fit.
1169    ///
1170    /// Based on Java's `Inventory.placeItemBackInInventory`.
1171    pub fn add_item_or_drop(&self, mut item: ItemStack, prediction: Prediction) {
1172        if item.is_empty() {
1173            return;
1174        }
1175
1176        let added = self.inventory.lock().add(&mut item);
1177        if !added || !item.is_empty() {
1178            let _ = self.drop_item(item, false, prediction);
1179        }
1180    }
1181
1182    /// Tries to add an item to the player's inventory using an existing lock guard,
1183    /// dropping it if it doesn't fit.
1184    ///
1185    /// Use this variant when you already hold a `ContainerLockGuard` that includes
1186    /// the player's inventory to avoid deadlocks.
1187    pub fn add_item_or_drop_with_guard(
1188        &self,
1189        guard: &mut ContainerLockGuard,
1190        mut item: ItemStack,
1191        prediction: Prediction,
1192    ) {
1193        if item.is_empty() {
1194            return;
1195        }
1196
1197        let inv_id = ContainerId::from_arc(&self.inventory);
1198        let should_drop = if let Some(inv) = guard.get_mut(inv_id) {
1199            let added = inv.add(&mut item);
1200            !added || !item.is_empty()
1201        } else {
1202            true
1203        };
1204        if should_drop {
1205            let _ = guard.run_unlocked(|| self.drop_item(item, false, prediction));
1206        }
1207    }
1208}