steel_core/behavior/items/
shears.rs1use 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#[item_behavior]
17pub struct ShearsItem;
18
19impl ShearsItem {
20 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 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}