steel_core/behavior/items/
bucket.rs1use crate::behavior::context::InteractionResult;
3use crate::behavior::item_utils::create_filled_result;
4use crate::behavior::{
5 BLOCK_BEHAVIORS, BlockStateBehaviorExt, FLUID_BEHAVIORS, ItemBehavior, UseItemContext,
6 pickup_waterlogged_block,
7};
8use crate::entity::Entity;
9use crate::fluid::{fluid_state_to_block, is_lava_fluid, is_water_fluid};
10use crate::world::RaytraceAction;
11use steel_macros::item_behavior;
12use steel_registry::blocks::block_state_ext::BlockStateExt;
13use steel_registry::blocks::properties::Direction;
14use steel_registry::fluid::{FluidRef, FluidState};
15use steel_registry::item_stack::ItemStack;
16use steel_registry::level_events;
17use steel_registry::sound_events;
18use steel_registry::stat::vanilla_stat_types;
19use steel_registry::vanilla_blocks;
20use steel_registry::vanilla_fluids;
21use steel_registry::vanilla_game_events;
22use steel_registry::vanilla_items;
23use steel_utils::types::UpdateFlags;
24use steel_utils::{BlockPos, BlockStateId};
25
26use crate::world::game_event::GameEventContext;
27
28#[item_behavior]
30pub struct BucketItem {
31 #[json_arg(vanilla_fluids, ref, json = "content")]
32 content: FluidRef,
33}
34
35impl BucketItem {
36 #[must_use]
38 pub const fn new(content: FluidRef) -> Self {
39 Self { content }
40 }
41}
42
43impl ItemBehavior for BucketItem {
44 fn use_item(&self, context: &mut UseItemContext) -> InteractionResult {
45 if self.content.is_empty {
46 return use_empty_bucket(context);
47 }
48 use_filled_bucket(self.content, context)
49 }
50}
51
52fn filled_bucket_success_stack(context: &UseItemContext) -> ItemStack {
53 if context.player.has_infinite_materials() {
54 context
55 .inv
56 .with_item(|item| item.copy_with_count(item.count()))
57 } else {
58 ItemStack::new(&vanilla_items::BUCKET)
59 }
60}
61
62fn use_empty_bucket(context: &mut UseItemContext) -> InteractionResult {
63 let (start, end) = context.player.get_ray_endpoints();
64
65 let (hit_block, hit_direction) = context.world.raytrace(start, end, |pos, world| {
67 let state = world.get_block_state(pos);
68 let block = state.get_block();
69
70 if block == &vanilla_blocks::AIR {
71 return RaytraceAction::Pass;
72 }
73
74 let fluid_state = state.get_fluid_state();
75 if fluid_state.is_source() {
76 return RaytraceAction::ImmediateHit;
77 }
78 if !fluid_state.is_empty() {
80 return RaytraceAction::Pass;
81 }
82
83 RaytraceAction::CheckShape
84 });
85
86 let (Some(hit_pos), Some(direction)) = (hit_block, hit_direction) else {
88 return InteractionResult::Pass;
89 };
90
91 if !may_use_bucket_at(context, hit_pos, direction) {
92 return InteractionResult::Fail;
93 }
94
95 let hit_state = context.world.get_block_state(hit_pos);
96 let block_behavior = BLOCK_BEHAVIORS.get_behavior(hit_state.get_block());
97
98 let picked = block_behavior
100 .pickup_block(context.world, hit_pos, hit_state, Some(context.player))
101 .or_else(|| {
102 pickup_waterlogged_block(
103 block_behavior,
104 context.world,
105 hit_pos,
106 hit_state,
107 Some(context.player),
108 )
109 });
110 let Some(result) = picked else {
112 return InteractionResult::Fail;
113 };
114
115 context.inv.with_item(|item| {
116 context
117 .player
118 .award_stat(&vanilla_stat_types::ITEM_USED, item.item());
119 });
120 if let Some(sound) = result.sound {
121 context.player.play_sound(sound, 1.0, 1.0);
122 }
123
124 create_filled_result(context, result.filled_bucket, true);
125 context.world.game_event(
126 &vanilla_game_events::FLUID_PICKUP,
127 hit_pos,
128 &GameEventContext::new(Some(context.player.as_ref()), None),
129 );
130
131 InteractionResult::Success
132}
133
134fn use_filled_bucket(fluid: FluidRef, context: &mut UseItemContext) -> InteractionResult {
135 let (start, end) = context.player.get_ray_endpoints();
136 let (ray_block, ray_dir) = context.world.raytrace(start, end, |pos, world| {
137 let state = world.get_block_state(pos);
138 let block = state.get_block();
139 if block == &vanilla_blocks::AIR {
142 return RaytraceAction::Pass;
143 }
144 RaytraceAction::CheckShape
145 });
146
147 let (Some(clicked_pos), Some(direction)) = (ray_block, ray_dir) else {
148 return InteractionResult::Pass;
149 };
150
151 if !context.world.is_in_valid_bounds(clicked_pos) {
152 return InteractionResult::Fail;
153 }
154
155 if !may_use_bucket_at(context, clicked_pos, direction) {
156 return InteractionResult::Fail;
157 }
158
159 let clicked_state = context.world.get_block_state(clicked_pos);
160 let is_sneaking = context.player.is_crouching();
161 let is_water_bucket = fluid == &vanilla_fluids::WATER;
163
164 let try_place_fluid = |pos: BlockPos, check_sneak: bool| -> bool {
166 if !context.world.is_in_valid_bounds(pos) {
167 return false;
168 }
169
170 let state = context.world.get_block_state(pos);
171 let fluid_state = state.get_fluid_state();
172
173 if check_sneak && is_sneaking && !state.get_block().config.is_air {
176 return false;
177 }
178
179 let behavior = BLOCK_BEHAVIORS.get_behavior(state.get_block());
180 let is_liquid_container = state.is_liquid_container();
181 let can_place_liquid = is_liquid_container
182 && behavior.can_place_liquid_with_player(state, fluid, Some(context.player));
183 let can_replace = state.can_be_replaced_by_fluid(fluid);
184
185 if !can_replace && !can_place_liquid {
186 return false;
187 }
188
189 if is_water_fluid(fluid) && context.world.dimension_type.water_evaporates {
192 context
193 .world
194 .level_event(level_events::PARTICLES_WATER_EVAPORATING, pos, 0, None);
195 return true;
196 }
197
198 if is_water_bucket && is_liquid_container {
199 let source_water = FluidState::source(&vanilla_fluids::WATER);
200 behavior.place_liquid(context.world, pos, state, source_water);
201 play_empty_sound_and_event(context, pos, fluid);
202 return true;
203 }
204
205 let is_same_fluid = FLUID_BEHAVIORS
208 .get_behavior(fluid)
209 .is_same(fluid_state.fluid_id);
210
211 if is_same_fluid && fluid_state.is_source() {
212 play_empty_sound_and_event(context, pos, fluid);
213 return true;
214 }
215
216 if can_replace && !state.get_block().config.liquid && !state.get_block().config.is_air {
218 context.player.get_world().destroy_block(pos, true);
219 }
220
221 let fluid_state_to_place = fluid_state_to_block(FluidState::source(fluid));
222 if context
223 .world
224 .set_block(pos, fluid_state_to_place, UpdateFlags::UPDATE_ALL_IMMEDIATE)
225 {
226 let tick_delay = FLUID_BEHAVIORS
227 .get_behavior(fluid)
228 .tick_delay(context.world);
229 context
230 .world
231 .schedule_fluid_tick_default(pos, fluid, tick_delay);
232
233 play_empty_sound_and_event(context, pos, fluid);
234
235 return true;
236 }
237 false
238 };
239
240 let primary_pos =
243 filled_bucket_primary_pos(clicked_state, clicked_pos, direction, is_water_bucket);
244
245 if !try_place_fluid(primary_pos, true)
246 && !try_place_fluid(direction.relative(clicked_pos), false)
247 {
248 return InteractionResult::Fail;
249 }
250
251 let result_stack = filled_bucket_success_stack(context);
252 create_filled_result(context, result_stack, true);
253 InteractionResult::Success
254}
255
256fn may_use_bucket_at(context: &UseItemContext, pos: BlockPos, direction: Direction) -> bool {
258 let item = context.inv.with_item(|item| item.clone());
259 context
260 .player
261 .may_use_item_at(direction.relative(pos), direction, &item)
262}
263
264fn play_empty_sound_and_event(context: &UseItemContext, pos: BlockPos, fluid: FluidRef) {
265 let sound_event = if is_lava_fluid(fluid) {
266 &sound_events::ITEM_BUCKET_EMPTY_LAVA
267 } else {
268 &sound_events::ITEM_BUCKET_EMPTY
269 };
270 context.world.play_block_sound(
271 sound_event,
272 pos,
273 1.0,
274 1.0,
275 Some(context.player.id()),
277 );
278 context.world.game_event(
279 &vanilla_game_events::FLUID_PLACE,
280 pos,
281 &GameEventContext::new(Some(context.player.as_ref()), None),
282 );
283}
284
285fn filled_bucket_primary_pos(
286 clicked_state: BlockStateId,
287 clicked_pos: BlockPos,
288 direction: Direction,
289 is_water_bucket: bool,
290) -> BlockPos {
291 if is_water_bucket && clicked_state.is_liquid_container() {
292 clicked_pos
293 } else {
294 direction.relative(clicked_pos)
295 }
296}
297
298#[cfg(test)]
299mod tests {
300 use crate::{
301 behavior::init_behaviors,
302 player::game_mode::use_item,
303 test_support::{TestPlayerBuilder, fresh_test_world, insert_ready_full_chunk},
304 };
305 use glam::DVec3;
306 use std::sync::Arc;
307 use steel_registry::{
308 RegistryHolderSet,
309 data_component_predicate::DataComponentMatchers,
310 data_components::{
311 AdventureModePredicate, BlockPredicate, vanilla_components::CAN_PLACE_ON,
312 },
313 init_vanilla_registry,
314 item_stack::ItemStack,
315 vanilla_blocks, vanilla_items,
316 };
317 use steel_utils::{
318 BlockPos, ChunkPos,
319 types::{GameType, InteractionHand, UpdateFlags},
320 };
321
322 use super::*;
323
324 #[test]
325 fn filled_water_bucket_targets_non_waterlogged_liquid_container_in_place() {
326 init_vanilla_registry();
327 init_behaviors();
328
329 let kelp = vanilla_blocks::KELP.default_state();
330
331 assert_eq!(
332 filled_bucket_primary_pos(kelp, BlockPos::ZERO, Direction::North, true),
333 BlockPos::ZERO
334 );
335 assert_eq!(
336 filled_bucket_primary_pos(kelp, BlockPos::ZERO, Direction::North, false),
337 Direction::North.relative(BlockPos::ZERO)
338 );
339 }
340
341 #[test]
342 fn adventure_buckets_require_can_place_on_and_fail_when_denied() {
343 init_vanilla_registry();
344 init_behaviors();
345 let world_fixture = fresh_test_world("adventure_bucket_restriction");
346 let world = &world_fixture.world;
347 let pos = BlockPos::new(0, 65, 2);
348 insert_ready_full_chunk(world, ChunkPos::from_block_pos(pos));
349 assert!(world.set_block(
350 pos,
351 vanilla_blocks::WATER.default_state(),
352 UpdateFlags::UPDATE_ALL,
353 ));
354
355 let player = TestPlayerBuilder::new(Arc::clone(world), "TestPlayer", 1).build();
356 player.base().set_position_local(DVec3::new(0.5, 64.0, 0.5));
357 player.set_rotation((0.0, 0.0));
358 player.restore_game_modes(GameType::Adventure, None);
359 player
360 .abilities
361 .lock()
362 .update_for_game_mode(GameType::Adventure);
363
364 player
365 .inventory
366 .lock()
367 .set_selected_item(ItemStack::new(&vanilla_items::BUCKET));
368 assert_eq!(
369 use_item(&player, world, InteractionHand::MainHand),
370 InteractionResult::Fail
371 );
372 assert_eq!(
373 world.get_block_state(pos),
374 vanilla_blocks::WATER.default_state()
375 );
376
377 let predicate = BlockPredicate::new(
378 Some(RegistryHolderSet::Direct(vec![&vanilla_blocks::WATER])),
379 None,
380 None,
381 DataComponentMatchers::ANY,
382 );
383 let mut allowed_bucket = ItemStack::new(&vanilla_items::BUCKET);
384 allowed_bucket.set(
385 CAN_PLACE_ON,
386 AdventureModePredicate::new(vec![predicate]).expect("one block predicate is valid"),
387 );
388 player.inventory.lock().set_selected_item(allowed_bucket);
389 assert!(use_item(&player, world, InteractionHand::MainHand).consumes_action());
390 assert_eq!(
391 world.get_block_state(pos),
392 vanilla_blocks::AIR.default_state()
393 );
394
395 assert!(world.set_block(
396 pos,
397 vanilla_blocks::STONE.default_state(),
398 UpdateFlags::UPDATE_ALL,
399 ));
400 player
401 .inventory
402 .lock()
403 .set_selected_item(ItemStack::new(&vanilla_items::WATER_BUCKET));
404 assert_eq!(
405 use_item(&player, world, InteractionHand::MainHand),
406 InteractionResult::Fail
407 );
408 assert_eq!(
409 world.get_block_state(pos),
410 vanilla_blocks::STONE.default_state()
411 );
412 }
413}