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