Skip to main content

steel_core/behavior/items/
shears.rs

1//! Shears item behavior (`ShearsItem`).
2//!
3//! Right-clicking a growing plant head that is not at max age crops it, matching
4//! vanilla `ShearsItem.useOn`.
5
6use steel_macros::item_behavior;
7use steel_registry::{blocks::block_state_ext::BlockStateExt, sound_events, vanilla_game_events};
8use steel_utils::{BlockPos, BlockStateId, types::UpdateFlags};
9
10use crate::behavior::blocks::vegetation::GrowingPlantHeadBehavior;
11use crate::behavior::{BLOCK_BEHAVIORS, InteractionResult, ItemBehavior, UseOnContext};
12use crate::entity::Entity;
13use crate::world::game_event::GameEventContext;
14
15/// Behavior for shears.
16#[item_behavior]
17pub struct ShearsItem;
18
19impl ShearsItem {
20    /// Crops a growing plant head to max age so `random_tick` no longer extends it.
21    fn block_age(
22        context: &UseOnContext<'_>,
23        plant: &dyn GrowingPlantHeadBehavior,
24        pos: BlockPos,
25        state: BlockStateId,
26    ) {
27        let new_state = plant.get_max_age_state(state);
28        context
29            .world
30            .set_block(pos, new_state, UpdateFlags::UPDATE_ALL);
31        context.world.game_event(
32            &vanilla_game_events::BLOCK_CHANGE,
33            pos,
34            &GameEventContext::new(Some(context.player), Some(new_state)),
35        );
36    }
37}
38
39impl ItemBehavior for ShearsItem {
40    fn use_on(&self, context: &mut UseOnContext) -> InteractionResult {
41        let pos = context.hit_result.block_pos;
42        let state = context.world.get_block_state(pos);
43        let behavior = BLOCK_BEHAVIORS.get_behavior(state.get_block());
44        let Some(plant) = behavior.as_growing_plant_head() else {
45            return InteractionResult::Pass;
46        };
47        if plant.is_max_age(state) {
48            return InteractionResult::Pass;
49        }
50
51        // TODO: Trigger ITEM_USED_ON_BLOCK once advancement criteria dispatch is implemented.
52        context.world.play_block_sound(
53            &sound_events::BLOCK_GROWING_PLANT_CROP,
54            pos,
55            1.0,
56            1.0,
57            Some(context.player.id()),
58        );
59        Self::block_age(context, plant, pos, state);
60
61        let has_infinite_materials = context.player.has_infinite_materials();
62        context
63            .inv
64            .with_item(|item| item.hurt_and_break(1, has_infinite_materials));
65
66        InteractionResult::Success
67    }
68}
69
70#[cfg(test)]
71mod tests {
72    use std::io::Cursor;
73    use std::sync::Arc;
74
75    use glam::DVec3;
76    use steel_protocol::packet_traits::{CompressionInfo, EncodedPacket};
77    use steel_registry::blocks::block_state_ext::BlockStateExt;
78    use steel_registry::blocks::properties::BlockStateProperties;
79    use steel_registry::game_events::GameEventRef;
80    use steel_registry::item_stack::ItemStack;
81    use steel_registry::packets::play::C_SOUND;
82    use steel_registry::{sound_events, vanilla_blocks, vanilla_game_events, vanilla_items};
83    use steel_utils::codec::VarInt;
84    use steel_utils::locks::SyncMutex;
85    use steel_utils::serial::ReadFrom;
86    use steel_utils::types::{InteractionHand, UpdateFlags};
87    use steel_utils::{BlockPos, ChunkPos, Direction, SectionPos};
88    use text_components::TextComponent;
89
90    use super::ShearsItem;
91    use crate::behavior::blocks::vegetation::MAX_AGE;
92    use crate::behavior::{BlockHitResult, InteractionResult, ItemBehavior, UseOnContext};
93    use crate::bootstrap::init_globals;
94    use crate::chunk::chunk_holder::ChunkHolder;
95    use crate::entity::Entity;
96    use crate::player::connection::NetworkConnection;
97    use crate::player::{Player, PlayerConnection, ResetReason};
98    use crate::test_support::{TestPlayerBuilder, fresh_test_world, insert_ready_full_chunk};
99    use crate::world::World;
100    use crate::world::game_event::{GameEventContext, GameEventListener, SharedGameEventListener};
101
102    const LISTENER_RADIUS: i32 = 16;
103
104    struct RecordingConnection {
105        packets: Arc<SyncMutex<Vec<EncodedPacket>>>,
106    }
107
108    impl NetworkConnection for RecordingConnection {
109        fn compression(&self) -> Option<CompressionInfo> {
110            None
111        }
112
113        fn send_encoded(&self, packet: EncodedPacket) {
114            self.packets.lock().push(packet);
115        }
116
117        fn send_encoded_bundle(&self, packets: Vec<EncodedPacket>) {
118            self.packets.lock().extend(packets);
119        }
120
121        fn disconnect_with_reason(&self, _reason: TextComponent) {}
122
123        fn tick(&self) {}
124
125        fn latency(&self) -> i32 {
126            0
127        }
128
129        fn close(&self) {}
130
131        fn closed(&self) -> bool {
132            false
133        }
134    }
135
136    struct RecordingGameEventListener {
137        pos: DVec3,
138        events: Arc<SyncMutex<Vec<GameEventRef>>>,
139    }
140
141    impl GameEventListener for RecordingGameEventListener {
142        fn listener_pos(&self) -> Option<DVec3> {
143            Some(self.pos)
144        }
145
146        fn listener_radius(&self) -> i32 {
147            LISTENER_RADIUS
148        }
149
150        fn handle_game_event(
151            &self,
152            _world: &Arc<World>,
153            event: GameEventRef,
154            _context: &GameEventContext<'_>,
155            _source_pos: DVec3,
156        ) -> bool {
157            self.events.lock().push(event);
158            true
159        }
160    }
161
162    struct ShearsFixture {
163        world: Arc<World>,
164        _holder: Arc<ChunkHolder>,
165        player: Arc<Player>,
166        _observer: Arc<Player>,
167        packets: Arc<SyncMutex<Vec<EncodedPacket>>>,
168        events: Arc<SyncMutex<Vec<GameEventRef>>>,
169        pos: BlockPos,
170    }
171
172    fn block_center(pos: BlockPos) -> DVec3 {
173        pos.0.as_dvec3().map(|coordinate| coordinate + 0.5)
174    }
175
176    fn hit_result(pos: BlockPos) -> BlockHitResult {
177        BlockHitResult {
178            location: block_center(pos),
179            direction: Direction::Up,
180            block_pos: pos,
181            miss: false,
182            inside: false,
183            world_border_hit: false,
184        }
185    }
186
187    fn create_fixture(key: &'static str, age: u8) -> ShearsFixture {
188        init_globals();
189        let world = fresh_test_world(key);
190        let pos = BlockPos::new(8, 64, 8);
191        let holder = insert_ready_full_chunk(&world, ChunkPos::from_block_pos(pos));
192        let state = vanilla_blocks::KELP
193            .default_state()
194            .set_value(&BlockStateProperties::AGE_25, age);
195        assert!(world.set_block(pos, state, UpdateFlags::UPDATE_NONE));
196
197        let player = TestPlayerBuilder::new(Arc::clone(&world), "ShearsTester", 1).build();
198        player.inventory.lock().set_item_in_hand(
199            InteractionHand::MainHand,
200            ItemStack::new(&vanilla_items::SHEARS),
201        );
202
203        let packets = Arc::new(SyncMutex::new(Vec::new()));
204        let connection = Arc::new(PlayerConnection::Other(Box::new(RecordingConnection {
205            packets: Arc::clone(&packets),
206        })));
207        let observer = TestPlayerBuilder::new(Arc::clone(&world), "ShearsObserver", 2)
208            .connection(connection)
209            .build();
210        assert!(observer.try_set_position(block_center(pos)).is_ok());
211        assert!(world.add_player(Arc::clone(&observer), ResetReason::InitialJoin));
212        packets.lock().clear();
213
214        let events = Arc::new(SyncMutex::new(Vec::new()));
215        let listener: SharedGameEventListener = Arc::new(RecordingGameEventListener {
216            pos: block_center(pos),
217            events: Arc::clone(&events),
218        });
219        world.register_game_event_listener(SectionPos::from_block_pos(pos), listener);
220
221        ShearsFixture {
222            world,
223            _holder: holder,
224            player,
225            _observer: observer,
226            packets,
227            events,
228            pos,
229        }
230    }
231
232    fn sound_ids(packets: &SyncMutex<Vec<EncodedPacket>>) -> Vec<i32> {
233        packets
234            .lock()
235            .iter()
236            .filter_map(|packet| {
237                let mut cursor = Cursor::new(packet.encoded_data.as_slice());
238                VarInt::read(&mut cursor).ok()?;
239                if VarInt::read(&mut cursor).ok()?.0 != C_SOUND {
240                    return None;
241                }
242                Some(VarInt::read(&mut cursor).ok()?.0)
243            })
244            .collect()
245    }
246
247    fn use_shears(fixture: &ShearsFixture) -> InteractionResult {
248        let mut context = UseOnContext::new(
249            &fixture.player,
250            InteractionHand::MainHand,
251            hit_result(fixture.pos),
252            &fixture.world,
253            Arc::clone(&fixture.player.inventory),
254        );
255        ShearsItem.use_on(&mut context)
256    }
257
258    #[test]
259    fn use_on_crops_growing_head_and_emits_vanilla_effects() {
260        let non_max_age = MAX_AGE - 1;
261        let fixture = create_fixture("shears_crop", non_max_age);
262
263        assert_eq!(use_shears(&fixture), InteractionResult::Success);
264        assert_eq!(
265            fixture
266                .world
267                .get_block_state(fixture.pos)
268                .get_value(&BlockStateProperties::AGE_25),
269            MAX_AGE,
270        );
271        assert_eq!(
272            fixture
273                .player
274                .inventory
275                .lock()
276                .get_item_in_hand(InteractionHand::MainHand)
277                .get_damage_value(),
278            1,
279        );
280        assert_eq!(
281            sound_ids(&fixture.packets),
282            [sound_events::BLOCK_GROWING_PLANT_CROP.packet_holder_id()],
283        );
284        assert_eq!(
285            fixture.events.lock().as_slice(),
286            [&vanilla_game_events::BLOCK_CHANGE],
287        );
288    }
289
290    #[test]
291    fn use_on_max_age_head_passes_without_effects() {
292        let fixture = create_fixture("shears_max_age", MAX_AGE);
293        let original_state = fixture.world.get_block_state(fixture.pos);
294
295        assert_eq!(use_shears(&fixture), InteractionResult::Pass);
296        assert_eq!(fixture.world.get_block_state(fixture.pos), original_state);
297        assert_eq!(
298            fixture
299                .player
300                .inventory
301                .lock()
302                .get_item_in_hand(InteractionHand::MainHand)
303                .get_damage_value(),
304            0,
305        );
306        assert_eq!(sound_ids(&fixture.packets), Vec::<i32>::new());
307        let no_events: &[GameEventRef] = &[];
308        assert_eq!(fixture.events.lock().as_slice(), no_events);
309    }
310}