Skip to main content

steel_core/inventory/
lock.rs

1//! Container locking utilities for deadlock-free multi-container operations.
2//!
3//! This module provides types for safely locking multiple containers in a
4//! deterministic order to prevent deadlocks when performing operations that
5//! span multiple inventories (e.g., transferring items between containers).
6
7use parking_lot::ArcMutexGuard;
8use parking_lot::RawMutex;
9use rustc_hash::FxHashMap;
10use std::borrow::Borrow;
11use std::mem;
12use std::ops::{Deref, DerefMut};
13use std::sync::Arc;
14use steel_utils::locks::Shared;
15use steel_utils::{Downcast as _, DowncastType, locks::SyncMutex};
16
17use crate::{
18    block_entity::{BlockEntityBase, SharedBlockEntity},
19    inventory::container::Container,
20    player::{Player, player_inventory::PlayerInventory},
21};
22use steel_registry::item_stack::ItemStack;
23
24/// Thread-safe reference to an erased container.
25pub type SharedContainer = Shared<dyn Container>;
26
27struct LockedContainer(ArcMutexGuard<RawMutex, dyn Container>);
28
29impl Deref for LockedContainer {
30    type Target = dyn Container;
31
32    fn deref(&self) -> &Self::Target {
33        &*self.0
34    }
35}
36
37impl DerefMut for LockedContainer {
38    fn deref_mut(&mut self) -> &mut Self::Target {
39        &mut *self.0
40    }
41}
42
43/// A reference to a container that can be locked.
44///
45/// The optional owner is notified only after all container locks have been
46/// released at the corresponding Vanilla `setChanged` boundary.
47#[derive(Clone)]
48pub struct ContainerRef {
49    id: ContainerId,
50    source: SharedContainer,
51    owner: Option<Arc<BlockEntityBase>>,
52}
53
54impl<T> From<Shared<T>> for ContainerRef
55where
56    T: Container + 'static,
57{
58    fn from(container: Shared<T>) -> Self {
59        let id = ContainerId::from_arc(&container);
60        let container: SharedContainer = container;
61        Self {
62            id,
63            source: container,
64            owner: None,
65        }
66    }
67}
68
69impl<T> From<&Shared<T>> for ContainerRef
70where
71    T: Container + 'static,
72{
73    fn from(container: &Shared<T>) -> Self {
74        container.clone().into()
75    }
76}
77
78impl From<&ContainerRef> for ContainerRef {
79    fn from(r: &ContainerRef) -> Self {
80        r.clone()
81    }
82}
83
84impl From<&SharedContainer> for ContainerRef {
85    fn from(container: &SharedContainer) -> Self {
86        container.clone().into()
87    }
88}
89
90impl From<SharedContainer> for ContainerRef {
91    fn from(container: SharedContainer) -> Self {
92        Self {
93            id: ContainerId::from_arc(&container),
94            source: container,
95            owner: None,
96        }
97    }
98}
99
100impl ContainerRef {
101    /// Creates a `ContainerRef` from a block entity, if it implements Container.
102    ///
103    /// Returns `None` if the block entity has no container capability.
104    #[must_use]
105    pub fn from_block_entity(block_entity: SharedBlockEntity) -> Option<Self> {
106        block_entity.container_ref()
107    }
108
109    /// Creates a container capability owned by the block entity whose unique
110    /// base is returned from its [`crate::block_entity::BlockEntity::base`].
111    ///
112    /// The container implementation must obey [`Container`]'s storage-local
113    /// locking contract. This owner performs block-entity and comparator
114    /// notifications after all container locks are released.
115    #[must_use]
116    pub fn owned_by_block_entity(container: SharedContainer, owner: Arc<BlockEntityBase>) -> Self {
117        Self {
118            id: ContainerId::from_arc(&container),
119            source: container,
120            owner: Some(owner),
121        }
122    }
123
124    /// Returns a unique identifier for this container based on its Arc pointer address.
125    #[must_use]
126    pub const fn container_id(&self) -> ContainerId {
127        self.id
128    }
129
130    /// Checks container access without locking its item storage.
131    #[must_use]
132    pub fn still_valid(&self, player: &Player) -> bool {
133        self.owner
134            .as_ref()
135            .is_none_or(|owner| owner.is_valid_container_for(player))
136    }
137
138    /// Locks this container and returns a guard.
139    fn lock(&self) -> LockedContainer {
140        LockedContainer(SyncMutex::lock_arc(&self.source))
141    }
142}
143
144/// A guard that holds locks on multiple containers in a deterministic order.
145///
146/// This struct ensures that when multiple containers need to be locked simultaneously,
147/// they are always locked in the same order (by pointer address) to prevent deadlocks.
148///
149/// # Example
150///
151/// ```ignore
152/// let player_inv = ContainerRef::from(player_inv_arc);
153/// let chest = ContainerRef::from(chest_arc);
154///
155/// let mut guard = ContainerLockGuard::lock_all(&[&player_inv, &chest]);
156///
157/// // Access containers by their IDs
158/// let player_id = player_inv.container_id();
159/// if let Some(inv) = guard.get_mut(player_id) {
160///     // Modify the player inventory
161/// }
162/// ```
163pub struct ContainerLockGuard {
164    // Stable sources retained while guards are temporarily released for an owner callback.
165    sources: Vec<(ContainerId, ContainerRef)>,
166    // Store locked guards in deterministic order
167    guards: Vec<(ContainerId, LockedContainer)>,
168    // For quick lookup
169    id_to_index: FxHashMap<ContainerId, usize>,
170}
171
172impl ContainerLockGuard {
173    /// Create a new lock guard and lock all containers in deterministic order.
174    ///
175    /// Containers are sorted by their pointer address before locking to ensure
176    /// a consistent lock order across all call sites, preventing deadlocks.
177    /// Duplicate containers (same Arc) are automatically deduplicated.
178    #[must_use]
179    pub fn lock_all<C>(containers: &[C]) -> Self
180    where
181        C: Borrow<ContainerRef>,
182    {
183        let mut sources: Vec<_> = containers
184            .iter()
185            .map(|c| (c.borrow().container_id(), c.borrow().clone()))
186            .collect();
187
188        // Sort by ID for deterministic lock order (prevents deadlocks)
189        sources.sort_by_key(|(id, _)| *id);
190
191        // Deduplicate (in case same container passed multiple times)
192        sources.dedup_by_key(|(id, _)| *id);
193
194        // Lock all in sorted order
195        let mut guards = Vec::with_capacity(sources.len());
196        for (id, container) in &sources {
197            let guard = container.lock();
198            guards.push((*id, guard));
199        }
200
201        // Build index map
202        let id_to_index = guards
203            .iter()
204            .enumerate()
205            .map(|(idx, (id, _))| (*id, idx))
206            .collect();
207
208        Self {
209            sources,
210            guards,
211            id_to_index,
212        }
213    }
214
215    /// Get mutable access to N locked containers simultaneously
216    ///
217    /// Returns `None` if any ID is not locked or if any IDs are duplicates
218    pub fn get_disjoint_mut<const N: usize>(
219        &mut self,
220        ids: [ContainerId; N],
221    ) -> Option<[&mut dyn Container; N]> {
222        let mut indices = [0usize; N];
223        for (i, id) in ids.iter().enumerate() {
224            indices[i] = *self.id_to_index.get(id)?;
225        }
226        let entries = self.guards.get_disjoint_mut(indices).ok()?;
227        Some(entries.map(|(_, locked)| &mut **locked as &mut dyn Container))
228    }
229
230    /// Unlock all containers and relock with a new set.
231    ///
232    /// This should only be called when you need to add more containers
233    /// mid-operation. All existing references from `get()`/`get_mut()` are invalidated.
234    #[must_use]
235    pub fn relock(self, containers: &[&ContainerRef]) -> Self {
236        // Drop self, releasing all locks
237        drop(self);
238        // Lock new set
239        Self::lock_all(containers)
240    }
241
242    /// Get immutable access to a locked container.
243    #[must_use]
244    pub fn get(&self, id: impl Into<ContainerId>) -> Option<&dyn Container> {
245        self.id_to_index
246            .get(&id.into())
247            .and_then(|&idx| self.guards.get(idx))
248            .map(|(_, guard)| &**guard as &dyn Container)
249    }
250
251    /// Get mutable access to a locked container.
252    ///
253    /// This bypasses owner notification and is only for deliberate no-update
254    /// mutation or callers that establish the notification boundary separately.
255    pub fn get_mut(&mut self, id: impl Into<ContainerId>) -> Option<&mut dyn Container> {
256        self.id_to_index
257            .get(&id.into())
258            .copied()
259            .and_then(|idx| self.guards.get_mut(idx))
260            .map(|(_, guard)| &mut **guard as &mut dyn Container)
261    }
262
263    /// Mirrors a container's own `setItem` call.
264    ///
265    /// Vanilla block-entity containers call `BlockEntity::setChanged` from
266    /// `setItem`, before a `Slot::set` performs its separate notification.
267    pub fn set_item(&mut self, id: impl Into<ContainerId>, slot: usize, stack: ItemStack) -> bool {
268        let id = id.into();
269        let Some(&index) = self.id_to_index.get(&id) else {
270            return false;
271        };
272        self.guards[index].1.set_item(slot, stack);
273        let owner = self.sources[index].1.owner.clone();
274        self.notify_owner(owner);
275        true
276    }
277
278    /// Mirrors a container's own conditional `removeItem` notification.
279    pub fn remove_item(
280        &mut self,
281        id: impl Into<ContainerId>,
282        slot: usize,
283        amount: i32,
284    ) -> Option<ItemStack> {
285        let id = id.into();
286        let index = *self.id_to_index.get(&id)?;
287        let removed = self.guards.get_mut(index)?.1.remove_item(slot, amount);
288        if !removed.is_empty() {
289            let owner = self.sources.get(index)?.1.owner.clone();
290            self.notify_owner(owner);
291        }
292        Some(removed)
293    }
294
295    /// Calls `Container::set_changed` and synchronously notifies its owner
296    /// after releasing every lock held by this guard.
297    pub fn set_changed(&mut self, id: impl Into<ContainerId>) -> bool {
298        let id = id.into();
299        let Some(&index) = self.id_to_index.get(&id) else {
300            return false;
301        };
302        self.guards[index].1.set_changed();
303        let owner = self.sources[index].1.owner.clone();
304        self.notify_owner(owner);
305        true
306    }
307
308    /// Runs a callback after releasing every container, then reacquires the
309    /// same sources in deterministic order before returning.
310    pub(crate) fn run_unlocked<R>(&mut self, callback: impl FnOnce() -> R) -> R {
311        drop(mem::take(&mut self.guards));
312        let result = callback();
313        self.guards = self
314            .sources
315            .iter()
316            .map(|(id, container)| (*id, container.lock()))
317            .collect();
318        result
319    }
320
321    fn notify_owner(&mut self, owner: Option<Arc<BlockEntityBase>>) {
322        let Some(owner) = owner else {
323            return;
324        };
325
326        self.run_unlocked(|| owner.set_changed());
327    }
328
329    /// Gets immutable access when the locked container has concrete type `T`.
330    #[must_use]
331    pub fn get_typed<T>(&self, id: impl Into<ContainerId>) -> Option<&T>
332    where
333        T: Container + DowncastType,
334    {
335        self.get(id)?.downcast_ref::<T>()
336    }
337
338    /// Gets mutable access when the locked container has concrete type `T`.
339    ///
340    /// This bypasses owner notification and is only for deliberate no-update
341    /// mutation or callers that establish the notification boundary separately.
342    pub fn get_typed_mut<T>(&mut self, id: impl Into<ContainerId>) -> Option<&mut T>
343    where
344        T: Container + DowncastType,
345    {
346        self.get_mut(id)?.downcast_mut::<T>()
347    }
348
349    /// Gets mutable access to two distinct concrete containers.
350    ///
351    /// This bypasses owner notification and is only for deliberate no-update
352    /// mutation or callers that establish the notification boundary separately.
353    pub fn get_two_typed_mut<A, B>(
354        &mut self,
355        first: impl Into<ContainerId>,
356        second: impl Into<ContainerId>,
357    ) -> Option<(&mut A, &mut B)>
358    where
359        A: Container + DowncastType,
360        B: Container + DowncastType,
361    {
362        let first_index = *self.id_to_index.get(&first.into())?;
363        let second_index = *self.id_to_index.get(&second.into())?;
364        if first_index == second_index {
365            return None;
366        }
367
368        let (first, second): (&mut dyn Container, &mut dyn Container) =
369            if first_index < second_index {
370                let (before_second, from_second) = self.guards.split_at_mut(second_index);
371                (&mut *before_second[first_index].1, &mut *from_second[0].1)
372            } else {
373                let (before_first, from_first) = self.guards.split_at_mut(first_index);
374                (&mut *from_first[0].1, &mut *before_first[second_index].1)
375            };
376        Some((first.downcast_mut::<A>()?, second.downcast_mut::<B>()?))
377    }
378
379    /// Check if a container is locked.
380    #[must_use]
381    pub fn contains(&self, id: ContainerId) -> bool {
382        self.id_to_index.contains_key(&id)
383    }
384}
385
386/// Unique identifier for a container based on Arc pointer address.
387///
388/// This ID is used to establish a deterministic ordering when locking
389/// multiple containers, preventing deadlocks.
390#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
391pub struct ContainerId(usize);
392
393impl ContainerId {
394    /// Creates a container ID from an Arc's pointer address.
395    pub fn from_arc<T: ?Sized>(arc: &Arc<T>) -> Self {
396        Self(Arc::as_ptr(arc).cast::<()>() as usize)
397    }
398}
399
400impl From<&Shared<PlayerInventory>> for ContainerId {
401    fn from(value: &Shared<PlayerInventory>) -> Self {
402        Self::from_arc(value)
403    }
404}
405
406#[cfg(test)]
407mod tests {
408    use std::sync::{Arc, Weak};
409
410    use steel_registry::blocks::block_state_ext::BlockStateExt as _;
411    use steel_registry::blocks::properties::{BlockStateProperties, Direction};
412    use steel_registry::{
413        init_vanilla_registry, item_stack::ItemStack, vanilla_block_entity_types, vanilla_blocks,
414        vanilla_items,
415    };
416    use steel_utils::types::UpdateFlags;
417    use steel_utils::{BlockPos, ChunkPos, locks::SyncMutex};
418
419    use super::{ContainerId, ContainerLockGuard, ContainerRef};
420    use crate::behavior::{BLOCK_BEHAVIORS, init_behaviors};
421    use crate::block_entity::{
422        SharedBlockEntity,
423        entities::{BarrelBlockEntity, RawBlockEntity},
424        init_block_entities,
425    };
426    use crate::inventory::container::{Container, CraftingContainer, ResultContainer};
427    use crate::inventory::slots::{NormalSlot, Slot as _};
428    use crate::test_support::{fresh_test_world, insert_ready_full_chunk};
429
430    #[test]
431    fn erased_container_ref_preserves_id_and_typed_access() {
432        let crafting = Arc::new(SyncMutex::new(CraftingContainer::new(2, 2)));
433        let id = ContainerId::from_arc(&crafting);
434        let container_ref = ContainerRef::from(Arc::clone(&crafting));
435
436        assert_eq!(container_ref.container_id(), id);
437
438        let mut guard = ContainerLockGuard::lock_all(&[&container_ref]);
439        let Some(typed) = guard.get_typed::<CraftingContainer>(id) else {
440            panic!("erased crafting container should retain its concrete type");
441        };
442        assert_eq!((typed.width(), typed.height()), (2, 2));
443        assert!(guard.get_typed::<ResultContainer>(id).is_none());
444        assert!(guard.get_typed_mut::<CraftingContainer>(id).is_some());
445    }
446
447    #[test]
448    fn block_entity_container_capability_is_independently_lockable() {
449        init_vanilla_registry();
450        let barrel = Arc::new(BarrelBlockEntity::new(
451            Weak::new(),
452            BlockPos::new(1, 2, 3),
453            vanilla_blocks::BARREL.default_state(),
454        ));
455        let block_entity: SharedBlockEntity = barrel.clone();
456        let Some(container_ref) = ContainerRef::from_block_entity(block_entity) else {
457            panic!("barrel block entity should expose Container");
458        };
459        let id = container_ref.container_id();
460
461        let guard = ContainerLockGuard::lock_all(&[&container_ref]);
462        assert_eq!(guard.get(id).map(Container::get_container_size), Some(27));
463    }
464
465    #[test]
466    fn non_container_block_entity_ref_is_rejected() {
467        init_vanilla_registry();
468        let block_entity: SharedBlockEntity = Arc::new(RawBlockEntity::new(
469            &vanilla_block_entity_types::END_PORTAL,
470            Weak::new(),
471            BlockPos::new(1, 2, 3),
472            vanilla_blocks::END_PORTAL.default_state(),
473        ));
474
475        assert!(ContainerRef::from_block_entity(block_entity).is_none());
476    }
477
478    #[test]
479    fn unlocked_callback_releases_and_reacquires_every_container() {
480        let crafting = Arc::new(SyncMutex::new(CraftingContainer::new(2, 2)));
481        let result = Arc::new(SyncMutex::new(ResultContainer::new()));
482        let crafting_ref = ContainerRef::from(Arc::clone(&crafting));
483        let result_ref = ContainerRef::from(Arc::clone(&result));
484        let mut guard = ContainerLockGuard::lock_all(&[&crafting_ref, &result_ref]);
485
486        let both_unlocked = guard.run_unlocked(|| {
487            let crafting_guard = crafting.try_lock();
488            let result_guard = result.try_lock();
489            crafting_guard.is_some() && result_guard.is_some()
490        });
491
492        assert!(both_unlocked);
493        assert!(guard.get(crafting_ref.container_id()).is_some());
494        assert!(guard.get(result_ref.container_id()).is_some());
495    }
496
497    #[test]
498    fn barrel_change_reenters_analog_read_without_holding_container_lock() {
499        init_vanilla_registry();
500        init_behaviors();
501        init_block_entities();
502        let world = fresh_test_world("barrel_comparator_reentry");
503        let barrel_pos = BlockPos::new(8, 64, 8);
504        let comparator_pos = barrel_pos.west();
505        insert_ready_full_chunk(&world, ChunkPos::from_block_pos(barrel_pos));
506        assert!(world.set_block(
507            comparator_pos.below(),
508            vanilla_blocks::STONE.default_state(),
509            UpdateFlags::UPDATE_NONE,
510        ));
511        assert!(world.set_block(
512            barrel_pos,
513            vanilla_blocks::BARREL.default_state(),
514            UpdateFlags::UPDATE_NONE,
515        ));
516        let comparator_state = vanilla_blocks::COMPARATOR
517            .default_state()
518            .set_value(&BlockStateProperties::HORIZONTAL_FACING, Direction::East);
519        assert!(world.set_block(comparator_pos, comparator_state, UpdateFlags::UPDATE_NONE,));
520        assert!(!world.has_scheduled_block_tick(comparator_pos, &vanilla_blocks::COMPARATOR));
521
522        let container_ref = ContainerRef::from_block_entity(
523            world
524                .get_block_entity(barrel_pos)
525                .expect("barrel should create its block entity"),
526        )
527        .expect("barrel should expose a container capability");
528        let slot = NormalSlot::new(container_ref.clone(), 0);
529        let mut guard = ContainerLockGuard::lock_all(&[&container_ref]);
530        slot.set_item(&mut guard, ItemStack::new(&vanilla_items::STONE));
531        drop(guard);
532
533        let analog = BLOCK_BEHAVIORS
534            .get_behavior(&vanilla_blocks::BARREL)
535            .get_analog_output_signal(
536                world.get_block_state(barrel_pos),
537                world.as_ref(),
538                barrel_pos,
539                Direction::West,
540            );
541        assert_eq!(analog, 1);
542        assert!(
543            world.has_scheduled_block_tick(comparator_pos, &vanilla_blocks::COMPARATOR),
544            "barrel callback did not schedule the comparator despite analog output {analog}"
545        );
546    }
547}