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