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.as_ref()), 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::{
99        TestPlayerBuilder, TestWorld, fresh_test_world, insert_ready_full_chunk,
100    };
101    use crate::world::World;
102    use crate::world::game_event::{GameEventContext, GameEventListener, SharedGameEventListener};
103
104    const LISTENER_RADIUS: i32 = 16;
105
106    struct RecordingConnection {
107        packets: Arc<SyncMutex<Vec<EncodedPacket>>>,
108    }
109
110    impl NetworkConnection for RecordingConnection {
111        fn compression(&self) -> Option<CompressionInfo> {
112            None
113        }
114
115        fn send_encoded(&self, packet: EncodedPacket) {
116            self.packets.lock().push(packet);
117        }
118
119        fn send_encoded_bundle(&self, packets: Vec<EncodedPacket>) {
120            self.packets.lock().extend(packets);
121        }
122
123        fn disconnect_with_reason(&self, _reason: TextComponent) {}
124
125        fn tick(&self) {}
126
127        fn latency(&self) -> i32 {
128            0
129        }
130
131        fn close(&self) {}
132
133        fn closed(&self) -> bool {
134            false
135        }
136    }
137
138    struct RecordingGameEventListener {
139        pos: DVec3,
140        events: Arc<SyncMutex<Vec<GameEventRef>>>,
141    }
142
143    impl GameEventListener for RecordingGameEventListener {
144        fn listener_pos(&self) -> Option<DVec3> {
145            Some(self.pos)
146        }
147
148        fn listener_radius(&self) -> i32 {
149            LISTENER_RADIUS
150        }
151
152        fn handle_game_event(
153            &self,
154            _world: &Arc<World>,
155            event: GameEventRef,
156            _context: &GameEventContext<'_>,
157            _source_pos: DVec3,
158        ) -> bool {
159            self.events.lock().push(event);
160            true
161        }
162    }
163
164    struct ShearsFixture {
165        world_fixture: TestWorld,
166        _holder: Arc<ChunkHolder>,
167        player: Arc<Player>,
168        _observer: Arc<Player>,
169        packets: Arc<SyncMutex<Vec<EncodedPacket>>>,
170        events: Arc<SyncMutex<Vec<GameEventRef>>>,
171        pos: BlockPos,
172    }
173
174    fn block_center(pos: BlockPos) -> DVec3 {
175        pos.0.as_dvec3().map(|coordinate| coordinate + 0.5)
176    }
177
178    fn hit_result(pos: BlockPos) -> BlockHitResult {
179        BlockHitResult {
180            location: block_center(pos),
181            direction: Direction::Up,
182            block_pos: pos,
183            miss: false,
184            inside: false,
185            world_border_hit: false,
186        }
187    }
188
189    fn create_fixture(key: &'static str, age: u8) -> ShearsFixture {
190        init_globals();
191        let world_fixture = fresh_test_world(key);
192        let world = &world_fixture.world;
193        let pos = BlockPos::new(8, 64, 8);
194        let holder = insert_ready_full_chunk(world, ChunkPos::from_block_pos(pos));
195        let state = vanilla_blocks::KELP
196            .default_state()
197            .set_value(&BlockStateProperties::AGE_25, age);
198        assert!(world.set_block(pos, state, UpdateFlags::UPDATE_NONE));
199
200        let player = TestPlayerBuilder::new(Arc::clone(world), "ShearsTester", 1).build();
201        player.inventory.lock().set_item_in_hand(
202            InteractionHand::MainHand,
203            ItemStack::new(&vanilla_items::SHEARS),
204        );
205
206        let packets = Arc::new(SyncMutex::new(Vec::new()));
207        let connection = Arc::new(PlayerConnection::Other(Box::new(RecordingConnection {
208            packets: Arc::clone(&packets),
209        })));
210        let observer = TestPlayerBuilder::new(Arc::clone(world), "ShearsObserver", 2)
211            .connection(connection)
212            .build();
213        assert!(observer.try_set_position(block_center(pos)).is_ok());
214        assert!(world.add_player(Arc::clone(&observer), ResetReason::InitialJoin));
215        packets.lock().clear();
216
217        let events = Arc::new(SyncMutex::new(Vec::new()));
218        let listener: SharedGameEventListener = Arc::new(RecordingGameEventListener {
219            pos: block_center(pos),
220            events: Arc::clone(&events),
221        });
222        world.register_game_event_listener(SectionPos::from_block_pos(pos), listener);
223
224        ShearsFixture {
225            world_fixture,
226            _holder: holder,
227            player,
228            _observer: observer,
229            packets,
230            events,
231            pos,
232        }
233    }
234
235    fn sound_ids(packets: &SyncMutex<Vec<EncodedPacket>>) -> Vec<i32> {
236        packets
237            .lock()
238            .iter()
239            .filter_map(|packet| {
240                let mut cursor = Cursor::new(packet.encoded_data.as_slice());
241                VarInt::read(&mut cursor).ok()?;
242                if VarInt::read(&mut cursor).ok()?.0 != C_SOUND {
243                    return None;
244                }
245                Some(VarInt::read(&mut cursor).ok()?.0)
246            })
247            .collect()
248    }
249
250    fn use_shears(fixture: &ShearsFixture) -> InteractionResult {
251        let mut context = UseOnContext::new(
252            &fixture.player,
253            InteractionHand::MainHand,
254            hit_result(fixture.pos),
255            &fixture.world_fixture.world,
256            Arc::clone(&fixture.player.inventory),
257        );
258        ShearsItem.use_on(&mut context)
259    }
260
261    #[test]
262    fn use_on_crops_growing_head_and_emits_vanilla_effects() {
263        let non_max_age = MAX_AGE - 1;
264        let fixture = create_fixture("shears_crop", non_max_age);
265
266        assert_eq!(use_shears(&fixture), InteractionResult::Success);
267        assert_eq!(
268            fixture
269                .world_fixture
270                .world
271                .get_block_state(fixture.pos)
272                .get_value(&BlockStateProperties::AGE_25),
273            MAX_AGE,
274        );
275        assert_eq!(
276            fixture
277                .player
278                .inventory
279                .lock()
280                .get_item_in_hand(InteractionHand::MainHand)
281                .get_damage_value(),
282            1,
283        );
284        assert_eq!(
285            sound_ids(&fixture.packets),
286            [sound_events::BLOCK_GROWING_PLANT_CROP.packet_holder_id()],
287        );
288        assert_eq!(
289            fixture.events.lock().as_slice(),
290            [&vanilla_game_events::BLOCK_CHANGE],
291        );
292    }
293
294    #[test]
295    fn use_on_max_age_head_passes_without_effects() {
296        let fixture = create_fixture("shears_max_age", MAX_AGE);
297        let original_state = fixture.world_fixture.world.get_block_state(fixture.pos);
298
299        assert_eq!(use_shears(&fixture), InteractionResult::Pass);
300        assert_eq!(
301            fixture.world_fixture.world.get_block_state(fixture.pos),
302            original_state
303        );
304        assert_eq!(
305            fixture
306                .player
307                .inventory
308                .lock()
309                .get_item_in_hand(InteractionHand::MainHand)
310                .get_damage_value(),
311            0,
312        );
313        assert_eq!(sound_ids(&fixture.packets), Vec::<i32>::new());
314        let no_events: &[GameEventRef] = &[];
315        assert_eq!(fixture.events.lock().as_slice(), no_events);
316    }
317}