1use std::sync::Arc;
2
3use steel_macros::block_behavior;
4use steel_registry::{
5 REGISTRY,
6 blocks::{
7 BlockRef,
8 block_state_ext::BlockStateExt,
9 properties::{BlockStateProperties, BoolProperty, IntProperty},
10 shapes::SupportType,
11 },
12 entity_data::Direction,
13 fluid::FluidState,
14 items::item::BlockHitResult,
15 sound_events,
16 vanilla_block_tags::BlockTag,
17 vanilla_blocks, vanilla_fluids, vanilla_game_events,
18};
19use steel_utils::{
20 BlockPos,
21 types::{self, UpdateFlags},
22};
23
24use crate::{
25 behavior::{
26 BlockBehavior, BlockPlaceContext, InteractionResult, InventoryAccess,
27 block::{
28 default_can_be_replaced, schedule_placed_liquid_tick,
29 schedule_water_tick_if_waterlogged,
30 },
31 },
32 entity::{Entity, projectile::Projectile},
33 player::{self, Player},
34 world::{
35 ClipHitResult, LevelAccessor, LevelReader, ScheduledTickAccess, World,
36 game_event::GameEventContext,
37 },
38};
39
40const CANDLES_PROPERTY: &IntProperty = &BlockStateProperties::CANDLES;
41const LIT_PROPERTY: &BoolProperty = &BlockStateProperties::LIT;
42const WATERLOGGED: &BoolProperty = &BlockStateProperties::WATERLOGGED;
43const MAX_CANDLES: u8 = 4;
44
45#[block_behavior]
47pub struct CandleBlock {
48 block: BlockRef,
49}
50
51impl CandleBlock {
52 #[must_use]
54 pub const fn new(block: BlockRef) -> Self {
55 Self { block }
56 }
57
58 pub(super) fn projectile_lit_state(
59 state: steel_utils::BlockStateId,
60 projectile_is_on_fire: bool,
61 ) -> Option<steel_utils::BlockStateId> {
62 (projectile_is_on_fire
63 && state.try_get_value(WATERLOGGED) != Some(true)
64 && !state.get_value(LIT_PROPERTY))
65 .then(|| state.set_value(LIT_PROPERTY, true))
66 }
67
68 #[must_use]
70 pub fn is_lit(state: steel_utils::BlockStateId) -> bool {
71 let block = state.get_block();
72 (block.has_tag(&BlockTag::CANDLES) || block.has_tag(&BlockTag::CANDLE_CAKES))
73 && state.try_get_value(LIT_PROPERTY) == Some(true)
74 }
75
76 pub fn extinguish(
78 player: Option<&Player>,
79 state: steel_utils::BlockStateId,
80 level: &(impl LevelAccessor + ?Sized),
81 pos: BlockPos,
82 ) {
83 level.set_block_state(
84 pos,
85 state.set_value(LIT_PROPERTY, false),
86 UpdateFlags::UPDATE_ALL_IMMEDIATE,
87 );
88 level.play_block_sound(&sound_events::BLOCK_CANDLE_EXTINGUISH, pos, 1.0, 1.0, None);
89 level.game_event(
90 &vanilla_game_events::BLOCK_CHANGE,
91 pos,
92 &GameEventContext::new(player.map(|player| player as &dyn Entity), None),
93 );
94 }
95}
96
97impl BlockBehavior for CandleBlock {
98 fn can_survive(
100 &self,
101 _state: steel_utils::BlockStateId,
102 world: &dyn LevelReader,
103 pos: BlockPos,
104 ) -> bool {
105 let below_pos = pos.below();
106 world.is_face_sturdy_for(
107 world.get_block_state(below_pos),
108 below_pos,
109 Direction::Up,
110 SupportType::Center,
111 )
112 }
113
114 fn can_be_replaced(
115 &self,
116 state: steel_utils::BlockStateId,
117 context: &BlockPlaceContext<'_>,
118 ) -> bool {
119 (!context.is_secondary_use_active()
120 && context.with_item(|item| item.item() == REGISTRY.items.by_block(self.block))
121 && state.get_value(CANDLES_PROPERTY) < MAX_CANDLES)
122 || default_can_be_replaced(state, context)
123 }
124
125 fn get_state_for_placement(
126 &self,
127 context: &BlockPlaceContext<'_>,
128 ) -> Option<steel_utils::BlockStateId> {
129 let state = context.world.get_block_state(context.place_pos());
130 if state.get_block() == self.block {
131 return Some(state.set_value(
132 CANDLES_PROPERTY,
133 (state.get_value(CANDLES_PROPERTY) + 1).min(MAX_CANDLES),
134 ));
135 }
136
137 let default_state = self.block.default_state();
138 self.can_survive(default_state, context.world.as_ref(), context.place_pos())
139 .then(|| default_state.set_value(WATERLOGGED, context.is_water_source()))
140 }
141
142 fn update_shape(
143 &self,
144 state: steel_utils::BlockStateId,
145 world: &dyn ScheduledTickAccess,
146 pos: BlockPos,
147 _direction: Direction,
148 _neighbor_pos: BlockPos,
149 _neighbor_state: steel_utils::BlockStateId,
150 ) -> steel_utils::BlockStateId {
151 schedule_water_tick_if_waterlogged(state, world, pos);
152
153 if !self.can_survive(state, world, pos) {
154 return REGISTRY.blocks.get_default_state_id(&vanilla_blocks::AIR);
155 }
156 state
157 }
158
159 fn on_projectile_hit(
160 &self,
161 state: steel_utils::BlockStateId,
162 world: &Arc<World>,
163 hit: &ClipHitResult,
164 projectile: &dyn Projectile,
165 ) {
166 let Some(lit_state) = Self::projectile_lit_state(state, projectile.is_on_fire()) else {
167 return;
168 };
169 world.set_block(hit.block_pos, lit_state, UpdateFlags::UPDATE_ALL_IMMEDIATE);
170 }
171
172 fn use_item_on(
173 &self,
174 state: steel_utils::BlockStateId,
175 world: &Arc<World>,
176 pos: BlockPos,
177 player: &player::Player,
178 _hand: types::InteractionHand,
179 _hit_result: &BlockHitResult,
180 inv: &mut InventoryAccess,
181 ) -> InteractionResult {
182 let item_is_empty = inv.with_item(|item_stack| item_stack.is_empty());
183 if item_is_empty && player.abilities.lock().may_build && state.get_value(LIT_PROPERTY) {
184 Self::extinguish(Some(player), state, world, pos);
185 return InteractionResult::Success;
186 }
187
188 InteractionResult::TryEmptyHandInteraction
189 }
190
191 fn place_liquid(
192 &self,
193 level: &dyn LevelAccessor,
194 pos: BlockPos,
195 state: steel_utils::BlockStateId,
196 fluid_state: FluidState,
197 ) -> bool {
198 if state.try_get_value(WATERLOGGED) != Some(false)
199 || fluid_state.fluid_id != &vanilla_fluids::WATER
200 {
201 return false;
202 }
203
204 let waterlogged = state.set_value(WATERLOGGED, true);
205 if state.get_value(LIT_PROPERTY) {
206 Self::extinguish(None, waterlogged, level, pos);
207 } else {
208 level.set_block_state(pos, waterlogged, UpdateFlags::UPDATE_ALL);
209 }
210
211 schedule_placed_liquid_tick(level, pos, fluid_state);
212 true
213 }
214}
215
216#[cfg(test)]
217mod tests {
218 use super::*;
219 use glam::DVec3;
220 use steel_registry::{init_vanilla_registry, item_stack::ItemStack, vanilla_items};
221 use steel_utils::ChunkPos;
222
223 use crate::{
224 behavior::{PlacementOrientation, PlacementSource, init_behaviors},
225 test_support::{TestLevel, fresh_test_world, insert_ready_full_chunk, test_world},
226 };
227
228 fn supporting_level() -> TestLevel {
229 TestLevel::default().with_block(
230 BlockPos::ZERO.below(),
231 vanilla_blocks::STONE.default_state(),
232 )
233 }
234
235 fn place_context<'a>(
236 world: &'a Arc<World>,
237 hit_pos: BlockPos,
238 direction: Direction,
239 item_in_hand: &'a mut ItemStack,
240 is_secondary_use_active: bool,
241 ) -> BlockPlaceContext<'a> {
242 let hit_result = BlockHitResult {
243 location: DVec3::ZERO,
244 direction,
245 block_pos: hit_pos,
246 miss: false,
247 inside: false,
248 world_border_hit: false,
249 };
250 let source = PlacementSource::direct(
251 None,
252 types::InteractionHand::MainHand,
253 item_in_hand,
254 PlacementOrientation::Player {
255 rotation: 0.0,
256 pitch: 0.0,
257 },
258 is_secondary_use_active,
259 );
260 BlockPlaceContext::new(world, source, &hit_result)
261 }
262
263 #[test]
264 fn waterlogged_candle_update_shape_schedules_water_tick() {
265 init_vanilla_registry();
266
267 let candle = CandleBlock::new(&vanilla_blocks::CANDLE);
268 let state = vanilla_blocks::CANDLE
269 .default_state()
270 .set_value(WATERLOGGED, true);
271 let level = supporting_level();
272
273 assert_eq!(
274 candle.update_shape(
275 state,
276 &level,
277 BlockPos::ZERO,
278 Direction::North,
279 Direction::North.relative(BlockPos::ZERO),
280 vanilla_blocks::AIR.default_state(),
281 ),
282 state
283 );
284 assert_eq!(
285 level
286 .scheduled_fluid_ticks
287 .borrow()
288 .iter()
289 .map(|tick| tick.fluid)
290 .collect::<Vec<_>>(),
291 vec![&vanilla_fluids::WATER]
292 );
293 }
294
295 #[test]
296 fn burning_projectile_lights_only_unlit_candles() {
297 init_vanilla_registry();
298
299 let unlit = vanilla_blocks::CANDLE
300 .default_state()
301 .set_value(LIT_PROPERTY, false)
302 .set_value(WATERLOGGED, false);
303 let lit = unlit.set_value(LIT_PROPERTY, true);
304 let waterlogged = unlit.set_value(WATERLOGGED, true);
305
306 assert_eq!(CandleBlock::projectile_lit_state(unlit, true), Some(lit));
307 assert_eq!(CandleBlock::projectile_lit_state(unlit, false), None);
308 assert_eq!(CandleBlock::projectile_lit_state(lit, true), None);
309 assert_eq!(CandleBlock::projectile_lit_state(waterlogged, true), None);
310 }
311
312 #[test]
313 fn water_placement_on_lit_candle_emits_block_change_event() {
314 init_vanilla_registry();
315
316 let candle = CandleBlock::new(&vanilla_blocks::CANDLE);
317 let state = vanilla_blocks::CANDLE
318 .default_state()
319 .set_value(WATERLOGGED, false)
320 .set_value(LIT_PROPERTY, true);
321 let level = supporting_level();
322
323 assert!(candle.place_liquid(
324 &level,
325 BlockPos::ZERO,
326 state,
327 FluidState::source(&vanilla_fluids::WATER),
328 ));
329
330 assert_eq!(
331 level
332 .block_sounds
333 .borrow()
334 .iter()
335 .map(|sound| sound.sound)
336 .collect::<Vec<_>>(),
337 vec![&sound_events::BLOCK_CANDLE_EXTINGUISH]
338 );
339 assert_eq!(
340 level
341 .game_events
342 .borrow()
343 .iter()
344 .map(|event| event.event)
345 .collect::<Vec<_>>(),
346 vec![&vanilla_game_events::BLOCK_CHANGE]
347 );
348 assert!(
349 level
350 .last_placed_state()
351 .expect("candle should be waterlogged")
352 .get_value(WATERLOGGED)
353 );
354 }
355
356 #[test]
357 fn is_lit_accepts_only_lit_candles_and_candle_cakes() {
358 init_vanilla_registry();
359
360 let candle = vanilla_blocks::CANDLE.default_state();
361 let candle_cake = vanilla_blocks::CANDLE_CAKE.default_state();
362
363 assert!(CandleBlock::is_lit(candle.set_value(LIT_PROPERTY, true)));
364 assert!(!CandleBlock::is_lit(candle.set_value(LIT_PROPERTY, false)));
365 assert!(CandleBlock::is_lit(
366 candle_cake.set_value(LIT_PROPERTY, true)
367 ));
368 assert!(!CandleBlock::is_lit(
369 candle_cake.set_value(LIT_PROPERTY, false)
370 ));
371 assert!(!CandleBlock::is_lit(
372 vanilla_blocks::CAMPFIRE
373 .default_state()
374 .set_value(LIT_PROPERTY, true)
375 ));
376 }
377
378 #[test]
379 fn extinguish_matches_vanilla_side_effects() {
380 init_vanilla_registry();
381
382 let lit = vanilla_blocks::CANDLE_CAKE
383 .default_state()
384 .set_value(LIT_PROPERTY, true);
385 let level = TestLevel::default();
386
387 CandleBlock::extinguish(None, lit, &level, BlockPos::ZERO);
388
389 let placed = level.placed_blocks.borrow();
390 assert_eq!(placed.len(), 1);
391 assert_eq!(placed[0].state, lit.set_value(LIT_PROPERTY, false));
392 assert_eq!(placed[0].flags, UpdateFlags::UPDATE_ALL_IMMEDIATE);
393
394 let sounds = level.block_sounds.borrow();
395 assert_eq!(sounds.len(), 1);
396 assert_eq!(sounds[0].sound, &sound_events::BLOCK_CANDLE_EXTINGUISH);
397 assert_eq!(sounds[0].exclude, None);
398
399 let events = level.game_events.borrow();
400 assert_eq!(events.len(), 1);
401 assert_eq!(events[0].event, &vanilla_game_events::BLOCK_CHANGE);
402 assert_eq!(events[0].affected_state, None);
403 }
404
405 #[test]
406 fn candle_can_be_replaced_matching_stack() {
407 init_vanilla_registry();
408 init_behaviors();
409 let world = test_world();
410 let candle = CandleBlock::new(&vanilla_blocks::CANDLE);
411 let state_1 = vanilla_blocks::CANDLE.default_state();
412 let state_4 = state_1.set_value(CANDLES_PROPERTY, 4);
413
414 let mut candle_item = ItemStack::new(&vanilla_items::CANDLE);
415 let ctx = place_context(
416 world,
417 BlockPos::ZERO,
418 Direction::Up,
419 &mut candle_item,
420 false,
421 );
422 assert!(candle.can_be_replaced(state_1, &ctx));
423
424 let mut candle_item_sneak = ItemStack::new(&vanilla_items::CANDLE);
425 let ctx_sneak = place_context(
426 world,
427 BlockPos::ZERO,
428 Direction::Up,
429 &mut candle_item_sneak,
430 true,
431 );
432 assert!(!candle.can_be_replaced(state_1, &ctx_sneak));
433
434 let mut candle_item_full = ItemStack::new(&vanilla_items::CANDLE);
435 let ctx_full = place_context(
436 world,
437 BlockPos::ZERO,
438 Direction::Up,
439 &mut candle_item_full,
440 false,
441 );
442 assert!(!candle.can_be_replaced(state_4, &ctx_full));
443
444 let mut diff_item = ItemStack::new(&vanilla_items::STONE);
445 let ctx_diff = place_context(world, BlockPos::ZERO, Direction::Up, &mut diff_item, false);
446 assert!(!candle.can_be_replaced(state_1, &ctx_diff));
447
448 let mut other_candle = ItemStack::new(&vanilla_items::RED_CANDLE);
449 let ctx_other = place_context(
450 world,
451 BlockPos::ZERO,
452 Direction::Up,
453 &mut other_candle,
454 false,
455 );
456 assert!(!candle.can_be_replaced(state_1, &ctx_other));
457 }
458
459 #[test]
460 fn candle_placement_state_increments_and_preserves_properties() {
461 init_vanilla_registry();
462 init_behaviors();
463 let world_fixture = fresh_test_world("candle_placement_increments");
464 let world = &world_fixture.world;
465 let candle = CandleBlock::new(&vanilla_blocks::CANDLE);
466
467 let pos = BlockPos::new(0, 10, 0);
468 insert_ready_full_chunk(world, ChunkPos::from_block_pos(pos));
469
470 let existing = vanilla_blocks::CANDLE
471 .default_state()
472 .set_value(CANDLES_PROPERTY, 2)
473 .set_value(LIT_PROPERTY, true)
474 .set_value(WATERLOGGED, false);
475 world.set_block(pos, existing, UpdateFlags::UPDATE_ALL_IMMEDIATE);
476
477 let mut item = ItemStack::new(&vanilla_items::CANDLE);
478 let ctx = place_context(world, pos, Direction::Up, &mut item, false);
479 let placed_state = candle
480 .get_state_for_placement(&ctx)
481 .expect("placement state should be present");
482 assert_eq!(placed_state.get_value(CANDLES_PROPERTY), 3);
483 assert!(placed_state.get_value(LIT_PROPERTY));
484 assert!(!placed_state.get_value(WATERLOGGED));
485 }
486
487 #[test]
488 fn candle_can_place_when_clicking_block_below() {
489 init_vanilla_registry();
490 init_behaviors();
491 let world_fixture = fresh_test_world("candle_click_block_below");
492 let world = &world_fixture.world;
493 let stone_pos = BlockPos::new(0, 10, 0);
494 let candle_pos = stone_pos.above();
495
496 insert_ready_full_chunk(world, ChunkPos::from_block_pos(stone_pos));
497
498 world.set_block(
499 stone_pos,
500 vanilla_blocks::STONE.default_state(),
501 UpdateFlags::UPDATE_ALL_IMMEDIATE,
502 );
503 world.set_block(
504 candle_pos,
505 vanilla_blocks::CANDLE.default_state(),
506 UpdateFlags::UPDATE_ALL_IMMEDIATE,
507 );
508
509 let mut candle_item = ItemStack::new(&vanilla_items::CANDLE);
510 let ctx = place_context(world, stone_pos, Direction::Up, &mut candle_item, false);
511
512 assert_eq!(ctx.hit_pos(), stone_pos);
513 assert_eq!(ctx.place_pos(), candle_pos);
514 assert!(!ctx.replaces_clicked_block());
515 assert!(ctx.can_place());
516
517 let candle = CandleBlock::new(&vanilla_blocks::CANDLE);
518 let new_state = candle
519 .get_state_for_placement(&ctx)
520 .expect("placement state should be present");
521 assert_eq!(new_state.get_value(CANDLES_PROPERTY), 2);
522 }
523}