Skip to main content

steel_core/inventory/menu/kinds/
chest_menu.rs

1//! Chest menu for chest-like containers (chests, barrels, ender chests, shulker boxes).
2//!
3//! 1-6 rows of 9 slots. Layout:
4//! - Slots 0 to `rows * 9 - 1`: Container
5//! - Slots `rows * 9` to `rows * 9 + 26`: Main inventory (27)
6//! - Slots `rows * 9 + 27` to `rows * 9 + 35`: Hotbar (9)
7
8use steel_registry::menu_type::MenuTypeRef;
9use steel_registry::vanilla_menu_types;
10
11use crate::inventory::prelude::*;
12use crate::player::player_inventory::PlayerInventory;
13
14/// Builds a chest-like menu with `rows` rows of 9 slots plus the player inventory.
15///
16/// # Panics
17/// Panics if `rows` is 0 or greater than 6.
18#[must_use]
19pub fn chest(
20    inventory: Shared<PlayerInventory>,
21    container_id: u8,
22    container: impl Into<ContainerRef>,
23    rows: usize,
24) -> Menu {
25    let container = container.into();
26    chest_with_kind(
27        inventory,
28        container_id,
29        container.clone(),
30        rows,
31        ChestKind { container },
32    )
33}
34
35/// Builds the same layout as [`chest`] under a caller-supplied menu kind.
36///
37/// Chest-shaped menus whose validity or close cleanup is not the backing
38/// container's own (the ender chest, whose container belongs to the player
39/// rather than the block) share the layout through this.
40///
41/// # Panics
42/// Panics if `rows` is 0 or greater than 6.
43#[must_use]
44pub(crate) fn chest_with_kind(
45    inventory: Shared<PlayerInventory>,
46    container_id: u8,
47    container: impl Into<ContainerRef>,
48    rows: usize,
49    kind: impl MenuKind + 'static,
50) -> Menu {
51    let container = container.into();
52    assert!(
53        (1..=6).contains(&rows),
54        "Chest rows must be between 1 and 6"
55    );
56
57    let mut builder = MenuBuilder::new(menu_type_for_rows(rows), container_id);
58    let chest = builder.section(&container, rows * 9);
59    let player = builder.player_inventory(&inventory);
60
61    builder.route(chest, player.all(), FillDirection::Backward);
62    builder.route(player.all(), chest, FillDirection::Forward);
63
64    builder.build(kind)
65}
66
67/// Menu type for a chest of `rows` rows.
68///
69/// # Panics
70/// Panics if `rows` is 0 or greater than 6.
71#[must_use]
72pub fn menu_type_for_rows(rows: usize) -> MenuTypeRef {
73    match rows {
74        1 => &vanilla_menu_types::GENERIC_9X1,
75        2 => &vanilla_menu_types::GENERIC_9X2,
76        3 => &vanilla_menu_types::GENERIC_9X3,
77        4 => &vanilla_menu_types::GENERIC_9X4,
78        5 => &vanilla_menu_types::GENERIC_9X5,
79        6 => &vanilla_menu_types::GENERIC_9X6,
80        _ => panic!("Invalid row count: {rows}"),
81    }
82}
83
84/// Per-menu chest state: just the backing container for the validity check.
85pub struct ChestKind {
86    /// The backing container.
87    container: ContainerRef,
88}
89
90// SAFETY: This Steel-owned key uniquely identifies the concrete menu kind
91// within the process.
92unsafe impl steel_utils::DowncastType for ChestKind {
93    const TYPE_KEY: steel_utils::DowncastTypeKey =
94        steel_utils::DowncastTypeKey::new("steel:menu/chest");
95}
96
97impl MenuKind for ChestKind {
98    /// Returns true if the backing container is still valid for the player.
99    fn still_valid(&self, _behavior: &MenuBehavior, player: &Player) -> bool {
100        self.container.still_valid(player)
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    use steel_utils::locks::IntoShared as _;
107
108    use super::*;
109    use crate::inventory::container::SimpleContainer;
110
111    #[test]
112    fn chest_uses_exactly_the_rows_requested_from_oversized_container() {
113        let inventory = PlayerInventory::new().into_shared();
114        let container = SimpleContainer::new(18).into_shared();
115
116        let menu = chest(inventory, 1, container, 1);
117
118        assert_eq!(menu.behavior().slot_count(), 9 + 36);
119    }
120}