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), 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::{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}