Skip to main content

steel_core/inventory/slots/
slot.rs

1//! Slot abstraction for inventory access.
2
3use steel_registry::item_stack::ItemStack;
4use steel_utils::ErasedType;
5
6use crate::inventory::lock::{ContainerId, ContainerLockGuard, ContainerRef};
7use crate::player::Player;
8
9/// Physical storage and auxiliary container dependencies used by a [`Slot`].
10///
11/// Menu builders derive their complete lock set and physical-alias checks from
12/// this descriptor, so custom slots must include every container accessed by
13/// any slot callback.
14pub struct SlotStorage {
15    physical: Option<(ContainerRef, usize)>,
16    dependencies: Vec<ContainerRef>,
17}
18
19impl SlotStorage {
20    /// Describes a slot backed by one physical container position.
21    #[must_use]
22    pub fn physical(container: impl Into<ContainerRef>, index: usize) -> Self {
23        Self {
24            physical: Some((container.into(), index)),
25            dependencies: Vec::new(),
26        }
27    }
28
29    /// Describes a storage-less slot that only accesses auxiliary containers.
30    #[must_use]
31    pub fn virtual_slot(dependencies: impl IntoIterator<Item = impl Into<ContainerRef>>) -> Self {
32        Self {
33            physical: None,
34            dependencies: dependencies.into_iter().map(Into::into).collect(),
35        }
36    }
37
38    /// Adds auxiliary containers accessed by slot callbacks.
39    #[must_use]
40    pub fn with_dependencies(
41        mut self,
42        dependencies: impl IntoIterator<Item = impl Into<ContainerRef>>,
43    ) -> Self {
44        self.dependencies
45            .extend(dependencies.into_iter().map(Into::into));
46        self
47    }
48
49    /// Returns the slot's physical container and container-local index.
50    #[must_use]
51    pub fn physical_backing(&self) -> Option<(&ContainerRef, usize)> {
52        self.physical
53            .as_ref()
54            .map(|(container, index)| (container, *index))
55    }
56
57    /// Returns the stable identity of the physical backing position.
58    #[must_use]
59    pub fn physical_key(&self) -> Option<(ContainerId, usize)> {
60        self.physical_backing()
61            .map(|(container, index)| (container.container_id(), index))
62    }
63
64    pub(crate) fn container_refs(&self) -> impl Iterator<Item = &ContainerRef> {
65        self.physical
66            .iter()
67            .map(|(container, _)| container)
68            .chain(self.dependencies.iter())
69    }
70}
71
72/// A view into a single position in a container, accessed via a `ContainerLockGuard`.
73///
74/// Concrete implementations must implement [`steel_utils::DowncastType`] with
75/// a unique, stable key so erased slot references can recover their type, and
76/// retain one [`SlotStorage`] descriptor for their full container dependency
77/// set.
78pub trait Slot: ErasedType + Send + Sync {
79    /// Returns this slot's physical storage and auxiliary dependencies.
80    fn storage(&self) -> &SlotStorage;
81
82    /// Returns a reference to the item in this slot.
83    fn get_item<'a>(&self, guard: &'a ContainerLockGuard) -> &'a ItemStack;
84
85    /// Returns a mutable reference to the item in this slot.
86    fn get_item_mut<'a>(&self, guard: &'a mut ContainerLockGuard) -> &'a mut ItemStack;
87
88    /// Sets the item in this slot.
89    fn set_item(&self, guard: &mut ContainerLockGuard, stack: ItemStack);
90
91    /// Sets the item, triggered by a player action. `previous` is the prior item.
92    fn set_by_player(
93        &self,
94        guard: &mut ContainerLockGuard,
95        stack: ItemStack,
96        _previous: &ItemStack,
97    ) {
98        self.set_item(guard, stack);
99    }
100
101    /// Returns true if this slot has an item.
102    fn has_item(&self, guard: &ContainerLockGuard) -> bool {
103        !self.get_item(guard).is_empty()
104    }
105
106    /// Returns true if the given item can be placed in this slot.
107    fn may_place(&self, _stack: &ItemStack) -> bool {
108        true
109    }
110
111    /// Returns true if items can be picked up from this slot.
112    fn may_pickup(&self, _guard: &ContainerLockGuard, _player: &Player) -> bool {
113        true
114    }
115
116    /// Returns true if partial removal is allowed from this slot.
117    fn allow_modification(&self, guard: &ContainerLockGuard, player: &Player) -> bool {
118        self.may_pickup(guard, player) && self.may_place(self.get_item(guard))
119    }
120
121    /// Returns the maximum stack size for this slot.
122    fn get_max_stack_size(&self, guard: &ContainerLockGuard) -> i32;
123
124    /// Returns the max stack size for `stack` here (min of slot and item limits).
125    fn get_max_stack_size_for_item(&self, guard: &ContainerLockGuard, stack: &ItemStack) -> i32 {
126        self.get_max_stack_size(guard).min(stack.max_stack_size())
127    }
128
129    /// Removes up to `amount` items from this slot and returns them.
130    fn remove(&self, guard: &mut ContainerLockGuard, amount: i32) -> ItemStack {
131        let item = self.get_item_mut(guard);
132        if item.is_empty() || amount <= 0 {
133            return ItemStack::empty();
134        }
135        item.split(amount)
136    }
137
138    /// Tries to remove items from this slot with validation.
139    fn try_remove(
140        &self,
141        guard: &mut ContainerLockGuard,
142        amount: i32,
143        max_amount: i32,
144        player: &Player,
145    ) -> Option<ItemStack> {
146        if !self.may_pickup(guard, player) {
147            return None;
148        }
149
150        let item_count = self.get_item(guard).count();
151
152        if !self.allow_modification(guard, player) && max_amount < item_count {
153            return None;
154        }
155
156        let take_amount = amount.min(max_amount);
157        let result = self.remove(guard, take_amount);
158        if result.is_empty() {
159            return None;
160        }
161
162        if self.get_item(guard).is_empty() {
163            self.set_by_player(guard, ItemStack::empty(), &result);
164        }
165
166        Some(result)
167    }
168
169    /// Called when an item is taken. Returns any remainder that couldn't be placed back.
170    fn on_take(
171        &self,
172        guard: &mut ContainerLockGuard,
173        _stack: &ItemStack,
174        _player: &Player,
175    ) -> Option<ItemStack> {
176        self.set_changed(guard);
177        None
178    }
179
180    /// Takes items with all checks and callbacks. Returns the items taken.
181    fn safe_take(
182        &self,
183        guard: &mut ContainerLockGuard,
184        amount: i32,
185        max_amount: i32,
186        player: &Player,
187    ) -> ItemStack {
188        if let Some(taken) = self.try_remove(guard, amount, max_amount, player) {
189            if let Some(remainder) = self.on_take(guard, &taken, player) {
190                player.add_item_or_drop_with_guard(guard, remainder);
191            }
192            taken
193        } else {
194            ItemStack::empty()
195        }
196    }
197
198    /// Inserts up to `amount` items, firing set callbacks.
199    fn safe_insert(
200        &self,
201        guard: &mut ContainerLockGuard,
202        mut input: ItemStack,
203        amount: i32,
204    ) -> ItemStack {
205        if input.is_empty() || !self.may_place(&input) {
206            return input;
207        }
208
209        let slot_stack = self.get_item(guard).clone();
210        let transferable = amount
211            .min(input.count)
212            .min(self.get_max_stack_size_for_item(guard, &input) - slot_stack.count);
213        if transferable <= 0 {
214            return input;
215        }
216
217        if slot_stack.is_empty() {
218            self.set_by_player(guard, input.split(transferable), &slot_stack);
219        } else if ItemStack::is_same_item_same_components(&slot_stack, &input) {
220            input.shrink(transferable);
221            let mut new_slot_stack = slot_stack.clone();
222            new_slot_stack.grow(transferable);
223            self.set_by_player(guard, new_slot_stack, &slot_stack);
224        }
225
226        input
227    }
228
229    /// Marks the slot's container as changed.
230    fn set_changed(&self, guard: &mut ContainerLockGuard);
231
232    /// Returns the container slot index.
233    fn get_container_slot(&self) -> usize;
234
235    /// Returns true if normal menu persistence and transfer rules must not be
236    /// applied to this slot.
237    ///
238    /// A container-backed fake slot must be the menu's only view of its
239    /// physical [`SlotStorage::physical_key`].
240    fn is_fake(&self) -> bool {
241        false
242    }
243}
244
245#[cfg(test)]
246mod tests {
247    use std::sync::{
248        Arc,
249        atomic::{AtomicBool, Ordering},
250    };
251
252    use steel_registry::{init_vanilla_registry, vanilla_items};
253    use steel_utils::locks::IntoShared;
254    use steel_utils::{Downcast as _, DowncastType, DowncastTypeKey};
255
256    use super::*;
257    use crate::inventory::slots::normal_slot::NormalSlot;
258    use crate::inventory::{container::SimpleContainer, lock::ContainerRef};
259
260    struct SafeInsertOverrideSlot {
261        base: NormalSlot,
262        called: Arc<AtomicBool>,
263    }
264
265    // SAFETY: This test-only key uniquely identifies `SafeInsertOverrideSlot`.
266    unsafe impl DowncastType for SafeInsertOverrideSlot {
267        const TYPE_KEY: DowncastTypeKey =
268            DowncastTypeKey::new("steel:test/slot/safe_insert_override");
269    }
270
271    impl Slot for SafeInsertOverrideSlot {
272        fn storage(&self) -> &SlotStorage {
273            self.base.storage()
274        }
275
276        fn get_item<'a>(&self, guard: &'a ContainerLockGuard) -> &'a ItemStack {
277            self.base.get_item(guard)
278        }
279
280        fn get_item_mut<'a>(&self, guard: &'a mut ContainerLockGuard) -> &'a mut ItemStack {
281            self.base.get_item_mut(guard)
282        }
283
284        fn set_item(&self, guard: &mut ContainerLockGuard, stack: ItemStack) {
285            self.base.set_item(guard, stack);
286        }
287
288        fn safe_insert(
289            &self,
290            _guard: &mut ContainerLockGuard,
291            input: ItemStack,
292            _amount: i32,
293        ) -> ItemStack {
294            self.called.store(true, Ordering::Relaxed);
295            input
296        }
297
298        fn get_max_stack_size(&self, guard: &ContainerLockGuard) -> i32 {
299            self.base.get_max_stack_size(guard)
300        }
301
302        fn set_changed(&self, guard: &mut ContainerLockGuard) {
303            self.base.set_changed(guard);
304        }
305
306        fn get_container_slot(&self) -> usize {
307            self.base.get_container_slot()
308        }
309    }
310
311    #[test]
312    fn custom_slot_safe_insert_override_survives_erasure() {
313        init_vanilla_registry();
314        let container = SimpleContainer::new(1).into_shared();
315        let container_ref = ContainerRef::from(Arc::clone(&container));
316        let called = Arc::new(AtomicBool::new(false));
317        let slot: Box<dyn Slot> = Box::new(SafeInsertOverrideSlot {
318            base: NormalSlot::new(container_ref.clone(), 0),
319            called: Arc::clone(&called),
320        });
321        assert!(slot.downcast_ref::<SafeInsertOverrideSlot>().is_some());
322        let mut guard = ContainerLockGuard::lock_all(&[container_ref]);
323
324        let remaining = slot.safe_insert(&mut guard, ItemStack::new(&vanilla_items::STONE), 1);
325
326        assert!(called.load(Ordering::Relaxed));
327        assert!(remaining.is(&vanilla_items::STONE));
328        assert!(slot.get_item(&guard).is_empty());
329    }
330}