Skip to main content

steel_core/inventory/slots/
restricted_slot.rs

1use std::sync::Arc;
2
3use steel_registry::item_stack::ItemStack;
4use steel_utils::{DowncastType, DowncastTypeKey};
5
6use crate::{
7    inventory::{
8        lock::{ContainerLockGuard, ContainerRef},
9        slots::{NormalSlot, Slot, SlotStorage},
10    },
11    player::Player,
12};
13
14type MayPlace = Box<dyn Fn(usize, &ItemStack) -> bool + Send + Sync>;
15type MayPickup = Box<dyn Fn(usize, &ItemStack, &ContainerLockGuard, &Player) -> bool + Send + Sync>;
16
17/// The predicates behind a [`RestrictedSlot`], shared by every slot of a
18/// section so a slot costs one pointer rather than one per predicate.
19pub struct RestrictedRules {
20    may_place: MayPlace,
21    may_pickup: Option<MayPickup>,
22}
23
24impl RestrictedRules {
25    pub(crate) fn place_only(
26        may_place: impl Fn(usize, &ItemStack) -> bool + Send + Sync + 'static,
27    ) -> Arc<Self> {
28        Arc::new(Self {
29            may_place: Box::new(may_place),
30            may_pickup: None,
31        })
32    }
33
34    pub(crate) fn guarded(
35        may_place: impl Fn(usize, &ItemStack) -> bool + Send + Sync + 'static,
36        may_pickup: impl Fn(usize, &ItemStack, &ContainerLockGuard, &Player) -> bool
37        + Send
38        + Sync
39        + 'static,
40    ) -> Arc<Self> {
41        Arc::new(Self {
42            may_place: Box::new(may_place),
43            may_pickup: Some(Box::new(may_pickup)),
44        })
45    }
46}
47
48/// A [`NormalSlot`] with custom place and pickup rules.
49pub struct RestrictedSlot {
50    base: NormalSlot,
51    rules: Arc<RestrictedRules>,
52}
53
54// SAFETY: This key uniquely identifies Steel's `RestrictedSlot`.
55unsafe impl DowncastType for RestrictedSlot {
56    const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:slot/restricted");
57}
58
59impl RestrictedSlot {
60    /// Placement gated by `may_place`, which receives the container-local slot
61    /// index; pickup stays allowed.
62    pub fn new(
63        container: impl Into<ContainerRef>,
64        index: usize,
65        may_place: impl Fn(usize, &ItemStack) -> bool + Send + Sync + 'static,
66    ) -> Self {
67        Self::with_rules(container, index, RestrictedRules::place_only(may_place))
68    }
69
70    /// Like [`new`](Self::new), but pickup is gated too.
71    pub fn guarded(
72        container: impl Into<ContainerRef>,
73        index: usize,
74        may_place: impl Fn(usize, &ItemStack) -> bool + Send + Sync + 'static,
75        may_pickup: impl Fn(usize, &ItemStack, &ContainerLockGuard, &Player) -> bool
76        + Send
77        + Sync
78        + 'static,
79    ) -> Self {
80        Self::with_rules(
81            container,
82            index,
83            RestrictedRules::guarded(may_place, may_pickup),
84        )
85    }
86
87    /// Shares one rules object across a whole section's slots.
88    pub(crate) fn with_rules(
89        container: impl Into<ContainerRef>,
90        index: usize,
91        rules: Arc<RestrictedRules>,
92    ) -> Self {
93        Self {
94            base: NormalSlot::new(container, index),
95            rules,
96        }
97    }
98}
99
100impl Slot for RestrictedSlot {
101    fn storage(&self) -> &SlotStorage {
102        self.base.storage()
103    }
104
105    fn get_item<'a>(&self, guard: &'a ContainerLockGuard) -> &'a ItemStack {
106        self.base.get_item(guard)
107    }
108
109    fn get_item_mut<'a>(&self, guard: &'a mut ContainerLockGuard) -> &'a mut ItemStack {
110        self.base.get_item_mut(guard)
111    }
112
113    fn set_item(&self, guard: &mut ContainerLockGuard, stack: ItemStack) {
114        self.base.set_item(guard, stack);
115    }
116
117    fn may_place(&self, stack: &ItemStack) -> bool {
118        (self.rules.may_place)(self.base.get_container_slot(), stack)
119    }
120
121    fn may_pickup(&self, guard: &ContainerLockGuard, player: &Player) -> bool {
122        self.rules.may_pickup.as_ref().is_none_or(|it| {
123            it(
124                self.base.get_container_slot(),
125                self.base.get_item(guard),
126                guard,
127                player,
128            )
129        })
130    }
131
132    fn get_max_stack_size(&self, guard: &ContainerLockGuard) -> i32 {
133        self.base.get_max_stack_size(guard)
134    }
135
136    fn set_changed(&self, guard: &mut ContainerLockGuard) {
137        self.base.set_changed(guard);
138    }
139
140    fn get_container_slot(&self) -> usize {
141        self.base.get_container_slot()
142    }
143}
144
145#[cfg(test)]
146mod tests {
147    use std::slice;
148    use std::sync::Arc;
149
150    use super::RestrictedSlot;
151    use crate::inventory::container::{Container, SimpleContainer};
152    use crate::inventory::lock::{ContainerLockGuard, ContainerRef};
153    use crate::inventory::slots::Slot as _;
154    use steel_registry::data_components::vanilla_components::MAX_STACK_SIZE;
155    use steel_registry::{init_vanilla_registry, item_stack::ItemStack, vanilla_items};
156    use steel_utils::locks::{IntoShared as _, SyncMutex};
157    use steel_utils::{DowncastType, DowncastTypeKey};
158
159    struct SingleItemContainer {
160        item: ItemStack,
161        max_stack_size: i32,
162    }
163
164    // SAFETY: This test-only key uniquely identifies `SingleItemContainer`.
165    unsafe impl DowncastType for SingleItemContainer {
166        const TYPE_KEY: DowncastTypeKey =
167            DowncastTypeKey::new("steel:test/container/restricted_slot_single_item");
168    }
169
170    impl Container for SingleItemContainer {
171        fn items(&self) -> &[ItemStack] {
172            slice::from_ref(&self.item)
173        }
174
175        fn items_mut(&mut self) -> &mut [ItemStack] {
176            slice::from_mut(&mut self.item)
177        }
178
179        fn get_max_stack_size(&self) -> i32 {
180            self.max_stack_size
181        }
182
183        fn set_changed(&mut self) {}
184    }
185
186    #[test]
187    fn max_stack_size_delegates_to_the_container_and_item() {
188        init_vanilla_registry();
189        let capped = Arc::new(SyncMutex::new(SingleItemContainer {
190            item: ItemStack::empty(),
191            max_stack_size: 1,
192        }));
193        let capped_ref = ContainerRef::from(capped);
194        let capped_slot = RestrictedSlot::new(capped_ref.clone(), 0, |_, _| true);
195        let mut capped_guard = ContainerLockGuard::lock_all(&[capped_ref]);
196        let capped_remainder = capped_slot.safe_insert(
197            &mut capped_guard,
198            ItemStack::with_count(&vanilla_items::STONE, 64),
199            64,
200        );
201        assert_eq!(capped_slot.get_item(&capped_guard).count(), 1);
202        assert_eq!(capped_remainder.count(), 63);
203
204        let default = SimpleContainer::new(1).into_shared();
205        let default_ref = ContainerRef::from(default);
206        let default_slot = RestrictedSlot::new(default_ref.clone(), 0, |_, _| true);
207        let mut default_guard = ContainerLockGuard::lock_all(&[default_ref]);
208        let mut stack = ItemStack::new(&vanilla_items::STONE);
209        stack.set(MAX_STACK_SIZE, 99);
210        stack.set_count(99);
211        let default_remainder = default_slot.safe_insert(&mut default_guard, stack, 99);
212
213        assert!(default_remainder.is_empty());
214        assert_eq!(default_slot.get_item(&default_guard).count(), 99);
215    }
216}