Skip to main content

steel_core/behavior/
context.rs

1//! Context types and results for block and item interactions.
2
3use glam::DVec3;
4use std::sync::Arc;
5use steel_math::{DEGREE_90, DEGREE_180, DEGREE_270};
6use steel_registry::blocks::properties::Direction;
7use steel_registry::item_stack::ItemStack;
8use steel_registry::items::ItemRef;
9use steel_utils::BlockPos;
10use steel_utils::locks::Shared;
11use steel_utils::types::InteractionHand;
12
13use crate::behavior::{BlockStateBehaviorExt, ITEM_BEHAVIORS};
14use crate::entity::{Entity, LivingEntity};
15use crate::fluid::FluidStateExt;
16use crate::inventory::equipment::EquipmentSlot;
17use crate::inventory::lock::{ContainerLockGuard, ContainerRef};
18use crate::player::Player;
19use crate::player::player_inventory::PlayerInventory;
20use crate::world::World;
21pub use steel_registry::items::item::BlockHitResult;
22
23/// Result of an interaction (item use, block use, etc.)
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum InteractionResult {
26    /// The interaction succeeded and consumed the action.
27    Success,
28    /// Successful blockside action without automatic item use effects
29    SuccessWithoutItem,
30    /// The interaction succeeded with a swing that the client did not predict
31    SuccessServer,
32    /// The interaction consumed the action without swinging.
33    Consume,
34    /// The interaction failed and consumed the action.
35    Fail,
36    /// The interaction did not apply; try the next handler.
37    Pass,
38    /// Try the empty-hand interaction on the block.
39    TryEmptyHandInteraction,
40}
41
42impl InteractionResult {
43    /// Returns true if this result consumes the action (Success or Fail).
44    /// Pass and `TryEmptyHandInteraction` do not consume the action.
45    #[must_use]
46    pub const fn consumes_action(self) -> bool {
47        matches!(
48            self,
49            InteractionResult::Success
50                | InteractionResult::SuccessWithoutItem
51                | InteractionResult::SuccessServer
52                | InteractionResult::Consume
53                | InteractionResult::Fail
54        )
55    }
56
57    /// Returns whether the interaction starts a predicted or serveronly swing
58    #[must_use]
59    pub const fn should_swing(self) -> bool {
60        matches!(
61            self,
62            InteractionResult::Success
63                | InteractionResult::SuccessWithoutItem
64                | InteractionResult::SuccessServer
65        )
66    }
67
68    /// Returns true for vanilla `InteractionResult.Success` variants that run
69    /// item-use side effects such as `minecraft:use_cooldown`.
70    #[must_use]
71    pub const fn should_apply_item_use_side_effects(self) -> bool {
72        matches!(
73            self,
74            InteractionResult::Success
75                | InteractionResult::SuccessServer
76                | InteractionResult::Consume
77        )
78    }
79}
80
81/// Context for placing a block.
82///
83/// Vanilla porting map:
84/// - `UseOnContext.getClickedPos()` is [`Self::hit_pos`].
85/// - `BlockPlaceContext.getClickedPos()` is [`Self::place_pos`].
86/// - `BlockPlaceContext.replacingClickedOnBlock()` is
87///   [`Self::replaces_clicked_block`].
88///
89/// When translating vanilla block placement code, do not map
90/// `BlockPlaceContext.getClickedPos()` to [`Self::hit_pos`].
91pub struct BlockPlaceContext<'a> {
92    /// Raw block position from the hit result.
93    ///
94    /// Vanilla equivalent: `UseOnContext.getClickedPos()`.
95    hit_pos: BlockPos,
96    /// The face of the block that was clicked.
97    clicked_face: Direction,
98    /// The exact location where the click occurred.
99    click_location: DVec3,
100    /// Whether the click was inside the block.
101    inside: bool,
102    /// Position where the block will be placed.
103    ///
104    /// Vanilla equivalent: `BlockPlaceContext.getClickedPos()`. Vanilla returns
105    /// the raw hit position only when replacing the clicked block; otherwise it
106    /// returns the adjacent block position in the clicked-face direction.
107    place_pos: BlockPos,
108    /// Whether placement replaces the hit block itself.
109    ///
110    /// Vanilla equivalent: `BlockPlaceContext.replacingClickedOnBlock()`.
111    replaces_clicked_block: bool,
112    /// The world where the block is being placed.
113    pub world: &'a Arc<World>,
114    source: PlacementSource<'a>,
115    mode: PlacementMode,
116}
117
118impl<'a> BlockPlaceContext<'a> {
119    /// Creates a placement context from a source and effective hit result.
120    #[must_use]
121    pub fn new(
122        world: &'a Arc<World>,
123        source: PlacementSource<'a>,
124        hit_result: &BlockHitResult,
125    ) -> Self {
126        Self::with_mode(world, source, hit_result, PlacementMode::Standard)
127    }
128
129    fn with_mode(
130        world: &'a Arc<World>,
131        source: PlacementSource<'a>,
132        hit_result: &BlockHitResult,
133        mode: PlacementMode,
134    ) -> Self {
135        let hit_pos = hit_result.block_pos;
136        let mut context = Self {
137            hit_pos,
138            clicked_face: hit_result.direction,
139            click_location: hit_result.location,
140            inside: hit_result.inside,
141            place_pos: hit_pos,
142            replaces_clicked_block: true,
143            world,
144            source,
145            mode,
146        };
147        context.resolve_placement_geometry();
148        context
149    }
150
151    /// Creates vanilla's playerless `DirectionalPlaceContext` equivalent.
152    #[must_use]
153    pub fn directional(
154        world: &'a Arc<World>,
155        pos: BlockPos,
156        direction: Direction,
157        stack: &'a mut ItemStack,
158        clicked_face: Direction,
159    ) -> Self {
160        let hit_result = BlockHitResult {
161            location: DVec3::new(
162                f64::from(pos.x()) + 0.5,
163                f64::from(pos.y()),
164                f64::from(pos.z()) + 0.5,
165            ),
166            direction: clicked_face,
167            block_pos: pos,
168            miss: false,
169            inside: false,
170            world_border_hit: false,
171        };
172        let source = PlacementSource::direct(
173            None,
174            InteractionHand::MainHand,
175            stack,
176            PlacementOrientation::Directional { direction },
177            false,
178        );
179        Self::with_mode(world, source, &hit_result, PlacementMode::Directional)
180    }
181
182    fn resolve_placement_geometry(&mut self) {
183        self.place_pos = self.hit_pos;
184        self.replaces_clicked_block = true;
185        self.replaces_clicked_block = self
186            .world
187            .get_block_state(self.hit_pos)
188            .can_be_replaced(self);
189        if self.mode == PlacementMode::Standard && !self.replaces_clicked_block {
190            self.place_pos = self.clicked_face.relative(self.hit_pos);
191        }
192    }
193
194    /// Returns whether the effective placement position can be replaced.
195    #[must_use]
196    pub fn can_place(&self) -> bool {
197        if self.mode == PlacementMode::Directional {
198            return self
199                .world
200                .get_block_state(self.hit_pos)
201                .can_be_replaced(self);
202        }
203
204        self.replaces_clicked_block
205            || self
206                .world
207                .get_block_state(self.place_pos)
208                .can_be_replaced(self)
209    }
210
211    /// Creates the shifted context used by vanilla `BlockPlaceContext.at`.
212    #[must_use]
213    pub fn at(mut self, pos: BlockPos, direction: Direction) -> Self {
214        let (step_x, step_y, step_z) = direction.offset();
215        self.hit_pos = pos;
216        self.clicked_face = direction;
217        self.click_location = DVec3::new(
218            f64::from(pos.x()) + 0.5 + f64::from(step_x) * 0.5,
219            f64::from(pos.y()) + 0.5 + f64::from(step_y) * 0.5,
220            f64::from(pos.z()) + 0.5 + f64::from(step_z) * 0.5,
221        );
222        self.inside = false;
223        self.mode = PlacementMode::Standard;
224        self.resolve_placement_geometry();
225        self
226    }
227
228    /// Returns the raw block position from the hit result.
229    #[must_use]
230    pub const fn hit_pos(&self) -> BlockPos {
231        self.hit_pos
232    }
233
234    /// Returns the face from the effective hit result.
235    #[must_use]
236    pub const fn clicked_face(&self) -> Direction {
237        self.clicked_face
238    }
239
240    /// Returns the exact effective hit location.
241    #[must_use]
242    pub const fn click_location(&self) -> DVec3 {
243        self.click_location
244    }
245
246    /// Returns whether the effective hit location is inside the hit block.
247    #[must_use]
248    pub const fn is_inside(&self) -> bool {
249        self.inside
250    }
251
252    /// Returns the effective block placement position.
253    #[must_use]
254    pub const fn place_pos(&self) -> BlockPos {
255        self.place_pos
256    }
257
258    /// Returns whether placement replaces the originally hit block.
259    #[must_use]
260    pub fn replaces_clicked_block(&self) -> bool {
261        if self.mode == PlacementMode::Directional {
262            self.can_place()
263        } else {
264            self.replaces_clicked_block
265        }
266    }
267
268    /// Returns the player associated with this placement, if any.
269    #[must_use]
270    pub const fn player(&self) -> Option<&Player> {
271        self.source.player()
272    }
273
274    /// Returns the interaction hand associated with this placement.
275    #[must_use]
276    pub const fn hand(&self) -> InteractionHand {
277        self.source.hand()
278    }
279
280    /// Runs `f` with read access to the current placement stack.
281    pub fn with_item<R>(&self, f: impl FnOnce(&ItemStack) -> R) -> R {
282        self.source.with_item(f)
283    }
284
285    /// Runs `f` with mutable access to the current placement stack.
286    pub fn with_item_mut<R>(&mut self, f: impl FnOnce(&mut ItemStack) -> R) -> R {
287        self.source.with_item_mut(f)
288    }
289
290    /// Returns the placement source used by block placement callbacks.
291    #[must_use]
292    pub const fn source(&self) -> &PlacementSource<'a> {
293        &self.source
294    }
295
296    /// Returns the horizontal placement direction.
297    #[must_use]
298    pub fn horizontal_direction(&self) -> Direction {
299        self.source.orientation.horizontal_direction()
300    }
301
302    /// Returns the placement rotation.
303    #[must_use]
304    pub const fn rotation(&self) -> f32 {
305        self.source.orientation.rotation()
306    }
307
308    /// Returns whether secondary use is active for this placement.
309    #[must_use]
310    pub const fn is_secondary_use_active(&self) -> bool {
311        self.source.is_secondary_use_active
312    }
313
314    /// Returns the direction the player is looking at most directly.
315    ///
316    /// This considers both yaw and pitch to determine the nearest direction
317    /// among all 6 directions (UP, DOWN, NORTH, SOUTH, EAST, WEST).
318    ///
319    /// Based on Java's `Direction.orderedByNearest(Entity)[0]`. Unlike
320    /// [`Self::get_nearest_looking_directions`], this does not reorder around
321    /// the clicked face when placing beside a non-replaceable block.
322    #[must_use]
323    pub fn get_nearest_looking_direction(&self) -> Direction {
324        self.source.orientation.nearest_looking_direction()
325    }
326
327    /// Returns the vertical direction the player is looking toward.
328    ///
329    /// Based on Java's `BlockPlaceContext.getNearestLookingVerticalDirection()`.
330    #[must_use]
331    pub const fn get_nearest_looking_vertical_direction(&self) -> Direction {
332        self.source.orientation.nearest_vertical_direction()
333    }
334    /// Returns all 6 directions ordered by how closely the player is looking at them.
335    ///
336    /// Based on Java's `BlockPlaceContext.getNearestLookingDirections()`.
337    /// When not replacing the clicked block, the opposite of the clicked face
338    /// is moved to the front of the array.
339    #[must_use]
340    pub fn get_nearest_looking_directions(&self) -> [Direction; 6] {
341        let (mut directions, adjust_for_replacement) = self.source.orientation.directions();
342
343        // If not replacing the clicked block, prioritize the opposite of clicked face
344        if adjust_for_replacement && !self.replaces_clicked_block {
345            let clicked_opposite = self.clicked_face.opposite();
346            if let Some(index) = directions.iter().position(|&d| d == clicked_opposite)
347                && index > 0
348            {
349                directions.copy_within(0..index, 1);
350                directions[0] = clicked_opposite;
351            }
352        }
353
354        directions
355    }
356
357    /// Returns true if the block at the placement position is a water source.
358    #[must_use]
359    pub fn is_water_source(&self) -> bool {
360        use crate::fluid::get_fluid_state;
361        let fluid_state = get_fluid_state(self.world, self.place_pos);
362        fluid_state.is_source() && fluid_state.is_water()
363    }
364
365    /// Returns true if the block at the placement position contains full water.
366    #[must_use]
367    pub fn is_full_water(&self) -> bool {
368        use crate::fluid::get_fluid_state;
369        let fluid_state = get_fluid_state(self.world, self.place_pos);
370        fluid_state.is_full() && fluid_state.is_water()
371    }
372}
373
374#[derive(Clone, Copy, Debug, Eq, PartialEq)]
375enum PlacementMode {
376    Standard,
377    Directional,
378}
379
380/// Placement direction behavior derived from either a player or a synthetic caller.
381#[derive(Clone, Copy, Debug)]
382pub enum PlacementOrientation {
383    /// Vanilla player-derived yaw and pitch.
384    Player {
385        /// Player yaw in degrees.
386        rotation: f32,
387        /// Player pitch in degrees.
388        pitch: f32,
389    },
390    /// Vanilla `DirectionalPlaceContext` orientation.
391    Directional {
392        /// Synthetic placement direction.
393        direction: Direction,
394    },
395}
396
397impl PlacementOrientation {
398    fn nearest_looking_direction(self) -> Direction {
399        match self {
400            Self::Player { rotation, pitch } => Direction::ordered_by_nearest(rotation, pitch)[0],
401            Self::Directional { .. } => Direction::Down,
402        }
403    }
404
405    fn horizontal_direction(self) -> Direction {
406        match self {
407            Self::Player { rotation, .. } => Direction::from_yaw(rotation),
408            Self::Directional { direction } if direction.is_horizontal() => direction,
409            Self::Directional { .. } => Direction::North,
410        }
411    }
412
413    const fn rotation(self) -> f32 {
414        match self {
415            Self::Player { rotation, .. } => rotation,
416            Self::Directional { direction } => match direction {
417                Direction::Down | Direction::Up => -DEGREE_90,
418                Direction::South => 0.0,
419                Direction::West => DEGREE_90,
420                Direction::North => DEGREE_180,
421                Direction::East => DEGREE_270,
422            },
423        }
424    }
425
426    const fn nearest_vertical_direction(self) -> Direction {
427        match self {
428            Self::Player { pitch, .. } if pitch < 0.0 => Direction::Up,
429            Self::Player { .. } | Self::Directional { .. } => Direction::Down,
430        }
431    }
432
433    fn directions(self) -> ([Direction; 6], bool) {
434        match self {
435            Self::Player { rotation, pitch } => {
436                (Direction::ordered_by_nearest(rotation, pitch), true)
437            }
438            Self::Directional { direction } => (directional_placement_directions(direction), false),
439        }
440    }
441}
442
443const fn directional_placement_directions(direction: Direction) -> [Direction; 6] {
444    match direction {
445        Direction::Down => [
446            Direction::Down,
447            Direction::North,
448            Direction::East,
449            Direction::South,
450            Direction::West,
451            Direction::Up,
452        ],
453        Direction::Up => [
454            Direction::Down,
455            Direction::Up,
456            Direction::North,
457            Direction::East,
458            Direction::South,
459            Direction::West,
460        ],
461        Direction::North => [
462            Direction::Down,
463            Direction::North,
464            Direction::East,
465            Direction::West,
466            Direction::Up,
467            Direction::South,
468        ],
469        Direction::South => [
470            Direction::Down,
471            Direction::South,
472            Direction::East,
473            Direction::West,
474            Direction::Up,
475            Direction::North,
476        ],
477        Direction::West => [
478            Direction::Down,
479            Direction::West,
480            Direction::South,
481            Direction::Up,
482            Direction::North,
483            Direction::East,
484        ],
485        Direction::East => [
486            Direction::Down,
487            Direction::East,
488            Direction::South,
489            Direction::Up,
490            Direction::North,
491            Direction::West,
492        ],
493    }
494}
495
496enum PlacementItemSource<'a> {
497    PlayerHand(InventoryAccess),
498    Direct(&'a mut ItemStack),
499}
500
501/// Player and item data retained across one block placement operation.
502///
503/// Player-backed access deliberately re-resolves the selected hand under short
504/// inventory locks. The stack may therefore change between accesses; retaining
505/// one inventory guard across behavior dispatch could deadlock callbacks which
506/// open a menu. Direct access retains the exact borrowed stack for the whole
507/// operation.
508pub struct PlacementSource<'a> {
509    player: Option<&'a Player>,
510    hand: InteractionHand,
511    item: PlacementItemSource<'a>,
512    orientation: PlacementOrientation,
513    is_secondary_use_active: bool,
514}
515
516impl<'a> PlacementSource<'a> {
517    /// Creates a placement source backed by a player's live hand.
518    #[must_use]
519    pub fn player_hand(player: &'a Player, inv: &InventoryAccess) -> Self {
520        let (rotation, pitch) = player.rotation();
521        let hand = inv.hand;
522        Self {
523            player: Some(player),
524            hand,
525            item: PlacementItemSource::PlayerHand(InventoryAccess::new(
526                Arc::clone(&inv.inventory),
527                hand,
528            )),
529            orientation: PlacementOrientation::Player { rotation, pitch },
530            is_secondary_use_active: player.is_secondary_use_active(),
531        }
532    }
533
534    /// Creates a source backed by a directly borrowed stack.
535    #[must_use]
536    pub const fn direct(
537        player: Option<&'a Player>,
538        hand: InteractionHand,
539        stack: &'a mut ItemStack,
540        orientation: PlacementOrientation,
541        is_secondary_use_active: bool,
542    ) -> Self {
543        Self {
544            player,
545            hand,
546            item: PlacementItemSource::Direct(stack),
547            orientation,
548            is_secondary_use_active,
549        }
550    }
551
552    /// Returns the player associated with this source, if any.
553    #[must_use]
554    pub const fn player(&self) -> Option<&Player> {
555        self.player
556    }
557
558    /// Returns the interaction hand associated with this source.
559    #[must_use]
560    pub const fn hand(&self) -> InteractionHand {
561        self.hand
562    }
563
564    /// Runs `f` with read access to this source's current stack.
565    pub fn with_item<R>(&self, f: impl FnOnce(&ItemStack) -> R) -> R {
566        match &self.item {
567            PlacementItemSource::PlayerHand(inv) => inv.with_item(|item| f(item)),
568            PlacementItemSource::Direct(item) => f(item),
569        }
570    }
571
572    /// Runs `f` with mutable access to this source's current stack.
573    pub fn with_item_mut<R>(&mut self, f: impl FnOnce(&mut ItemStack) -> R) -> R {
574        match &mut self.item {
575            PlacementItemSource::PlayerHand(inv) => inv.with_item(f),
576            PlacementItemSource::Direct(item) => f(item),
577        }
578    }
579}
580
581/// Access to the player's inventory.
582///
583/// This handle does not hold the inventory lock by itself. Use the closure
584/// methods to keep lock scopes short and avoid carrying an inventory guard
585/// through block behavior, world mutation, or menu opening.
586#[derive(Clone)]
587pub struct InventoryAccess {
588    inventory: Shared<PlayerInventory>,
589    hand: InteractionHand,
590}
591
592impl InventoryAccess {
593    /// Creates a new `InventoryAccess` instance.
594    pub const fn new(inventory: Shared<PlayerInventory>, hand: InteractionHand) -> Self {
595        Self { inventory, hand }
596    }
597
598    /// Runs `f` with mutable access to the item in the player's hand.
599    pub fn with_item<R>(&self, f: impl FnOnce(&mut ItemStack) -> R) -> R {
600        let mut inventory = self.inventory.lock();
601        inventory.mutate_item_in_hand(self.hand, f)
602    }
603
604    /// Runs `f` with mutable access to the player's inventory.
605    pub fn with_inventory<R>(&self, f: impl FnOnce(&mut PlayerInventory) -> R) -> R {
606        let mut inventory = self.inventory.lock();
607        f(&mut inventory)
608    }
609
610    /// Runs `f` with a container guard containing the player's inventory.
611    ///
612    /// Prefer [`Self::with_item`] or [`Self::with_inventory`] unless an operation
613    /// must interoperate with APIs that require `ContainerLockGuard`.
614    pub fn with_guard<R>(&self, f: impl FnOnce(&mut ContainerLockGuard) -> R) -> R {
615        let inv_ref = ContainerRef::from(Arc::clone(&self.inventory));
616        let mut guard = ContainerLockGuard::lock_all(&[&inv_ref]);
617        f(&mut guard)
618    }
619}
620
621/// Context for using an item on a block.
622///
623/// Immutable fields (`player`, `hand`, `world`, `hit_result`) can be accessed
624/// freely while `inv` is mutably borrowed — the borrow checker tracks them as
625/// disjoint fields.
626pub struct UseOnContext<'a> {
627    /// The player using the item.
628    pub player: &'a Arc<Player>,
629    /// Which hand the item is in.
630    pub hand: InteractionHand,
631    /// Information about where the block was hit.
632    pub hit_result: BlockHitResult,
633    /// The world where the interaction is happening.
634    pub world: &'a Arc<World>,
635    /// Mutable inventory access.
636    pub inv: InventoryAccess,
637}
638
639impl<'a> UseOnContext<'a> {
640    /// Creates a new `UseOnContext`.
641    #[must_use]
642    pub const fn new(
643        player: &'a Arc<Player>,
644        hand: InteractionHand,
645        hit_result: BlockHitResult,
646        world: &'a Arc<World>,
647        inventory: Shared<PlayerInventory>,
648    ) -> Self {
649        Self {
650            player,
651            hand,
652            hit_result,
653            world,
654            inv: InventoryAccess::new(inventory, hand),
655        }
656    }
657
658    /// Builds a [`BlockPlaceContext`] from this interaction context.
659    #[must_use]
660    pub fn build_place_context(&self) -> BlockPlaceContext<'a> {
661        BlockPlaceContext::new(
662            self.world,
663            PlacementSource::player_hand(self.player, &self.inv),
664            &self.hit_result,
665        )
666    }
667}
668
669/// Context for using an item (general usage).
670///
671/// Immutable fields (`player`, `hand`, `world`) can be accessed freely while
672/// `inv` is mutably borrowed.
673pub struct UseItemContext<'a> {
674    /// The player using the item.
675    pub player: &'a Arc<Player>,
676    /// Which hand the item is in.
677    pub hand: InteractionHand,
678    /// The world where the interaction is happening.
679    pub world: &'a Arc<World>,
680    /// Mutable inventory access.
681    pub inv: InventoryAccess,
682}
683
684impl<'a> UseItemContext<'a> {
685    /// Creates a new `UseItemContext`.
686    #[must_use]
687    pub const fn new(
688        player: &'a Arc<Player>,
689        hand: InteractionHand,
690        world: &'a Arc<World>,
691        inventory: Shared<PlayerInventory>,
692    ) -> Self {
693        Self {
694            player,
695            hand,
696            world,
697            inv: InventoryAccess::new(inventory, hand),
698        }
699    }
700}
701
702/// Context for an item inventory tick
703pub struct InventoryTickContext<'a> {
704    /// The current world
705    pub world: &'a Arc<World>,
706    /// The entity carrying the item
707    pub owner: &'a dyn Entity,
708    /// `None` if the slot is not selected and or in the main player inventory
709    pub slot: Option<EquipmentSlot>,
710    item: ItemRef,
711    source: InventoryTickSource<'a>,
712}
713
714enum InventoryTickSource<'a> {
715    PlayerInventory {
716        inventory: &'a Shared<PlayerInventory>,
717        index: usize,
718    },
719    Equipment {
720        owner: &'a dyn LivingEntity,
721        slot: EquipmentSlot,
722    },
723}
724
725impl<'a> InventoryTickContext<'a> {
726    pub(crate) fn tick_player_inventory(world: &'a Arc<World>, player: &'a Player) {
727        for index in 0..PlayerInventory::INVENTORY_SIZE {
728            let (item, selected) = {
729                let inventory = player.inventory.lock();
730                let stack = &inventory.get_items()[index];
731                if stack.is_empty() {
732                    continue;
733                }
734                (
735                    stack.item(),
736                    index == usize::from(inventory.get_selected_slot()),
737                )
738            };
739
740            let mut context = Self {
741                world,
742                owner: player,
743                slot: selected.then_some(EquipmentSlot::MainHand),
744                item,
745                source: InventoryTickSource::PlayerInventory {
746                    inventory: &player.inventory,
747                    index,
748                },
749            };
750            ITEM_BEHAVIORS
751                .get_behavior(item)
752                .inventory_tick(&mut context);
753        }
754    }
755
756    pub(crate) fn tick_equipment(
757        world: &'a Arc<World>,
758        owner: &'a dyn LivingEntity,
759        slots: impl IntoIterator<Item = EquipmentSlot>,
760    ) {
761        for slot in slots {
762            let mut item = None;
763            owner.with_equipment_slot(slot, &mut |stack| {
764                if !stack.is_empty() {
765                    item = Some(stack.item());
766                }
767            });
768            let Some(item) = item else {
769                continue;
770            };
771
772            let mut context = Self {
773                world,
774                owner,
775                slot: Some(slot),
776                item,
777                source: InventoryTickSource::Equipment { owner, slot },
778            };
779            ITEM_BEHAVIORS
780                .get_behavior(item)
781                .inventory_tick(&mut context);
782        }
783    }
784
785    /// Runs `f` on the stack that is being ticked, or returns `None` if the slot no longer contains it.
786    pub fn with_item<R>(&self, f: impl FnOnce(&mut ItemStack) -> R) -> Option<R> {
787        match self.source {
788            InventoryTickSource::PlayerInventory { inventory, index } => inventory
789                .lock()
790                .with_item_mut(index, |stack| stack.is(self.item).then(|| f(stack))),
791            InventoryTickSource::Equipment { owner, slot } => {
792                let mut f = Some(f);
793                let mut result = None;
794                owner.with_equipment_slot_mut(slot, &mut |stack| {
795                    if stack.is(self.item)
796                        && let Some(f) = f.take()
797                    {
798                        result = Some(f(stack));
799                    }
800                });
801                result
802            }
803        }
804    }
805}
806
807#[cfg(test)]
808mod tests {
809    use std::sync::Arc;
810
811    use steel_registry::data_components::vanilla_components::BLOCK_STATE;
812    use steel_registry::init_vanilla_registry;
813    use steel_registry::vanilla_items;
814    use steel_utils::locks::SyncMutex;
815
816    use super::*;
817    use crate::behavior::init_behaviors;
818    use crate::inventory::container::Container;
819    use crate::player::player_inventory::PlayerInventory;
820    use crate::test_support::test_world;
821
822    #[test]
823    fn item_use_side_effects_apply_to_all_success_variants() {
824        assert!(InteractionResult::Success.should_apply_item_use_side_effects());
825        assert!(InteractionResult::SuccessServer.should_apply_item_use_side_effects());
826        assert!(InteractionResult::Consume.should_apply_item_use_side_effects());
827        assert!(InteractionResult::SuccessWithoutItem.consumes_action());
828        assert!(!InteractionResult::SuccessWithoutItem.should_apply_item_use_side_effects());
829        assert!(InteractionResult::SuccessWithoutItem.should_swing());
830        assert!(!InteractionResult::Fail.should_apply_item_use_side_effects());
831        assert!(!InteractionResult::Pass.should_apply_item_use_side_effects());
832        assert!(!InteractionResult::TryEmptyHandInteraction.should_apply_item_use_side_effects());
833    }
834
835    #[test]
836    fn player_hand_source_reads_current_components_and_mutates_the_hand() {
837        init_vanilla_registry();
838
839        let inventory = Arc::new(SyncMutex::new(PlayerInventory::new()));
840        inventory
841            .lock()
842            .set_item(0, ItemStack::with_count(&vanilla_items::LIGHT, 2));
843        let access = InventoryAccess::new(Arc::clone(&inventory), InteractionHand::MainHand);
844        let mut source = PlacementSource {
845            player: None,
846            hand: InteractionHand::MainHand,
847            item: PlacementItemSource::PlayerHand(access),
848            orientation: PlacementOrientation::Player {
849                rotation: 0.0,
850                pitch: 0.0,
851            },
852            is_secondary_use_active: false,
853        };
854
855        assert!(source.with_item(|item| item.get(BLOCK_STATE).is_some()));
856        source.with_item_mut(ItemStack::shrink_one);
857        assert_eq!(
858            inventory
859                .lock()
860                .get_item_in_hand(InteractionHand::MainHand)
861                .count(),
862            1
863        );
864    }
865
866    #[test]
867    fn replacement_dispatch_does_not_hold_the_inventory_lock() {
868        init_vanilla_registry();
869        init_behaviors();
870
871        let inventory = Arc::new(SyncMutex::new(PlayerInventory::new()));
872        inventory
873            .lock()
874            .set_item(0, ItemStack::new(&vanilla_items::STONE));
875        let source = PlacementSource {
876            player: None,
877            hand: InteractionHand::MainHand,
878            item: PlacementItemSource::PlayerHand(InventoryAccess::new(
879                inventory,
880                InteractionHand::MainHand,
881            )),
882            orientation: PlacementOrientation::Player {
883                rotation: 0.0,
884                pitch: 0.0,
885            },
886            is_secondary_use_active: false,
887        };
888        let hit_result = BlockHitResult {
889            location: DVec3::ZERO,
890            direction: Direction::Up,
891            block_pos: BlockPos::new(0, 80, 0),
892            miss: false,
893            inside: false,
894            world_border_hit: false,
895        };
896
897        // Air replacement dispatch reads the live hand. Construction can only
898        // complete if no inventory guard is held across behavior dispatch.
899        let context = BlockPlaceContext::new(test_world(), source, &hit_result);
900        assert!(context.replaces_clicked_block());
901    }
902
903    #[test]
904    fn direct_source_mutates_the_callers_exact_stack() {
905        init_vanilla_registry();
906
907        let mut stack = ItemStack::with_count(&vanilla_items::LIGHT, 2);
908        {
909            let mut source = PlacementSource::direct(
910                None,
911                InteractionHand::MainHand,
912                &mut stack,
913                PlacementOrientation::Directional {
914                    direction: Direction::North,
915                },
916                false,
917            );
918            assert!(source.with_item(|item| item.get(BLOCK_STATE).is_some()));
919            source.with_item_mut(ItemStack::shrink_one);
920        }
921        assert_eq!(stack.count(), 1);
922    }
923
924    #[test]
925    fn at_changes_geometry_and_retains_the_direct_source() {
926        init_vanilla_registry();
927        init_behaviors();
928
929        let mut stack = ItemStack::new(&vanilla_items::STONE);
930        let hit_result = BlockHitResult {
931            location: DVec3::ZERO,
932            direction: Direction::Up,
933            block_pos: BlockPos::new(0, 80, 0),
934            miss: false,
935            inside: true,
936            world_border_hit: false,
937        };
938        let source = PlacementSource::direct(
939            None,
940            InteractionHand::MainHand,
941            &mut stack,
942            PlacementOrientation::Player {
943                rotation: 0.0,
944                pitch: 0.0,
945            },
946            false,
947        );
948        let context = BlockPlaceContext::new(test_world(), source, &hit_result);
949        let shifted_pos = BlockPos::new(4, 90, 7);
950        let mut shifted = context.at(shifted_pos, Direction::East);
951
952        assert_eq!(shifted.hit_pos(), shifted_pos);
953        assert_eq!(shifted.place_pos(), shifted_pos);
954        assert_eq!(shifted.clicked_face(), Direction::East);
955        assert_eq!(shifted.click_location(), DVec3::new(5.0, 90.5, 7.5));
956        assert!(!shifted.is_inside());
957        assert!(shifted.with_item(|item| item.is(&vanilla_items::STONE)));
958        shifted.with_item_mut(ItemStack::shrink_one);
959        drop(shifted);
960        assert!(stack.is_empty());
961    }
962
963    #[test]
964    fn directional_context_uses_vanilla_direction_order() {
965        init_vanilla_registry();
966        init_behaviors();
967
968        let mut stack = ItemStack::new(&vanilla_items::STONE);
969        let context = BlockPlaceContext::directional(
970            test_world(),
971            BlockPos::new(2, 80, 3),
972            Direction::West,
973            &mut stack,
974            Direction::Up,
975        );
976
977        assert!(context.player().is_none());
978        assert_eq!(context.horizontal_direction(), Direction::West);
979        assert_eq!(
980            context.get_nearest_looking_directions(),
981            [
982                Direction::Down,
983                Direction::West,
984                Direction::South,
985                Direction::Up,
986                Direction::North,
987                Direction::East,
988            ]
989        );
990    }
991
992    #[test]
993    fn singular_look_direction_is_not_reordered_around_clicked_face() {
994        init_vanilla_registry();
995        init_behaviors();
996
997        let mut stack = ItemStack::new(&vanilla_items::PISTON);
998        let source = PlacementSource::direct(
999            None,
1000            InteractionHand::MainHand,
1001            &mut stack,
1002            PlacementOrientation::Player {
1003                rotation: 0.0,
1004                pitch: 80.0,
1005            },
1006            false,
1007        );
1008        let hit_result = BlockHitResult {
1009            location: DVec3::ZERO,
1010            direction: Direction::East,
1011            block_pos: BlockPos::new(10, 80, 10),
1012            miss: false,
1013            inside: false,
1014            world_border_hit: false,
1015        };
1016        let mut context = BlockPlaceContext::new(test_world(), source, &hit_result);
1017        context.replaces_clicked_block = false;
1018
1019        assert_eq!(context.get_nearest_looking_direction(), Direction::Down);
1020        assert_eq!(context.get_nearest_looking_directions()[0], Direction::West);
1021    }
1022}