Skip to main content

steel_core/inventory/slots/
slot.rs

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