Skip to main content

steel_core/player/player_inventory/
player_handlers.rs

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